-
ActiveAdmin.register Demo do
-
includes :group
-
includes :author
-
-
filter :name
-
-
controller do
-
def permitted_params
-
params.permit!
-
end
-
end
-
-
form do |f|
-
f.inputs "Details" do
-
f.input :author_id
-
f.input :group_id
-
f.input :recorded_at
-
f.input :name
-
f.input :demo_handle
-
f.input :priority
-
f.input :description, as: :text
-
end
-
f.actions
-
end
-
-
actions :index, :show, :new, :edit, :update, :create
-
end
-
ActiveAdmin.register ForwardEmailRule do
-
filter :handle
-
filter :email
-
-
controller do
-
def permitted_params
-
params.permit!
-
end
-
end
-
-
form do |f|
-
f.inputs "Details" do
-
f.input :handle
-
f.input :email
-
end
-
f.actions
-
end
-
-
actions :index, :show, :new, :edit, :update, :create
-
end
-
ActiveAdmin.register Group, as: 'Group' do
-
controller do
-
def permitted_params
-
params.permit!
-
end
-
-
def find_resource
-
Group.friendly.find(params[:id])
-
end
-
end
-
-
actions :index, :show, :new, :edit, :create
-
-
filter :name
-
filter :handle, as: :string
-
filter :description
-
filter :memberships_count
-
filter :created_at
-
-
scope :parents_only
-
scope :not_demo
-
-
member_action :update, :method => :put do
-
group = Group.find(params[:id])
-
group.assign_attributes_and_files(permitted_params[:group])
-
privacy_change = GroupService::PrivacyChange.new(group)
-
group.save!
-
privacy_change.commit!
-
redirect_to admin_groups_path, :notice => "Group updated"
-
end
-
-
-
batch_action :delete_spam do |group_ids|
-
group_ids.each do |group_id|
-
if Group.any_trial.exists?(group_id)
-
group = Group.find(group_id)
-
if user = group.creator || group.admins.first
-
DestroyUserWorker.perform_async(user.id)
-
end
-
end
-
end
-
-
redirect_to admin_groups_path, notice: "#{group_ids.size} spammy groups deleted"
-
end
-
-
index :download_links => false do
-
selectable_column
-
column :id
-
column :name do |g|
-
g.full_name
-
end
-
-
column :privacy do |g|
-
g.group_privacy
-
end
-
-
column "Members", :memberships_count
-
column "Discussions", :discussions_count
-
column :created_at
-
column :description, :sortable => :description do |group|
-
group.description
-
end
-
column :revoked_at
-
column :analytics_enabled
-
actions
-
end
-
-
show do |group|
-
render 'graph', { group: group }
-
render 'stats', { group: group }
-
-
if defined?(SubscriptionService) && group.subscription_id
-
render 'subscription', { subscription: Subscription.for(group)}
-
end
-
-
if group.parent_id
-
panel("Parent group") do
-
link_to group.parent.name, admin_group_path(group.parent)
-
end
-
end
-
-
panel("Subgroups") do
-
table_for group.subgroups.order(memberships_count: :desc).each do |subgroup|
-
column :name do |g|
-
link_to g.name, admin_group_path(g)
-
end
-
column :memberships_count
-
column :discussions_count
-
end
-
end
-
-
panel("Members") do
-
table_for group.all_memberships.includes(:user, :inviter, :revoker).order(created_at: :desc).filter{|m| m.user }.each do |membership|
-
column(:name) { |m| link_to m.user.name, admin_user_path(m.user) }
-
column(:email) { |m| m.user.email }
-
column(:admin) { |m| m.admin }
-
column(:created_at) { |m| m.created_at }
-
column(:accepted_at) { |m| m.accepted_at }
-
column(:inviter) { |m| m.inviter.try(:name) }
-
column(:revoked_at) { |m| m.revoked_at }
-
column(:revoker) { |m| m.revoker.try(:name) }
-
column "Toggle admin" do |m|
-
if m.admin?
-
link_to("remove admin", remove_admin_admin_groups_path(membership_id: m.id, group_id: m.group_id), method: :post)
-
else
-
link_to("add admin", add_admin_admin_groups_path(membership_id: m.id, group_id: m.group_id), method: :post)
-
end
-
end
-
end
-
end
-
-
active_admin_comments
-
-
attributes_table do
-
row :id
-
row :name
-
row :key
-
row :full_name
-
row :created_at
-
row :updated_at
-
row :parent
-
row :creator_id
-
row :description
-
row :archived_at
-
row :discussions_count
-
row :memberships_count
-
row :admin_memberships_count
-
row :unverified_memberships_count
-
row :public_discussions_count
-
row :payment_plan
-
-
row "Group Privacy" do
-
if group.is_visible_to_public && group.discussion_privacy_options == 'public_only'
-
"Open"
-
elsif group.is_visible_to_public && group.discussion_privacy_options != 'public_only'
-
"Closed"
-
elsif group.is_visible_to_parent_members && !group.is_visible_to_public
-
"Closed"
-
elsif !group.is_visible_to_parent_members && !group.is_visible_to_public
-
"Secret"
-
else
-
"Group privacy unknown"
-
end
-
-
end
-
-
row :is_visible_to_public
-
row :discussion_privacy_options
-
row :is_visible_to_parent_members
-
row :parent_members_can_see_discussions
-
row :members_can_add_members
-
row :membership_granted_upon
-
row :members_can_edit_discussions
-
row :members_can_edit_comments
-
row :members_can_raise_motions
-
row :members_can_start_discussions
-
row :members_can_create_subgroups
-
row :handle
-
row :is_referral
-
row :subscription_id
-
row :theme_id
-
row :cover_photo_file_name
-
row :cover_photo_content_type
-
row :cover_photo_updated_at
-
row :logo_file_name
-
row :logo_content_type
-
row :logo_file_size
-
row :logo_updated_at
-
end
-
-
if group.archived_at.nil?
-
panel('Archive') do
-
link_to 'Archive this group', archive_admin_group_path(group), method: :post, data: {confirm: "Are you sure you wanna archive #{group.name}, pal?"}
-
end
-
else
-
panel('Unarchive') do
-
link_to 'Unarchive this group', unarchive_admin_group_path(group), method: :post, data: {confirm: "Are you sure you wanna unarchive #{group.name}, pal?"}
-
end
-
end
-
-
panel 'Move group' do
-
form action: move_admin_group_path(group), method: :post do |f|
-
f.input type: :hidden, name: :authenticity_token
-
f.label "Parent group id / key"
-
f.input name: :parent_id, value: group.parent_id
-
f.input type: :submit, value: "Move group"
-
end
-
end
-
-
render 'delete_group', { group: group }
-
render 'export_group', { group: group }
-
end
-
-
form do |f|
-
f.inputs "Details" do
-
if f.object.persisted?
-
f.input :id, :input_html => { :disabled => true }
-
end
-
f.input :name, :input_html => { :disabled => f.object.persisted? }
-
f.input :admin_tags, label: "Tags (separated by a space)"
-
f.input :description, :input_html => { :disabled => true }
-
f.input :parent_id, label: "Parent Id"
-
f.input :handle, as: :string
-
f.input :subscription_id, label: "Subscription Id"
-
f.input :is_visible_to_public, label: "Visible to public? (will change privacy of group, subgroups, discussions)"
-
f.input :membership_granted_upon, as: :select, collection: %w[request approval invitation]
-
end
-
f.actions
-
end
-
-
collection_action :import, method: :get do
-
end
-
-
collection_action :import_json, method: :post do
-
GenericWorker.perform_async('GroupExportService', 'import', params[:url])
-
redirect_to admin_groups_path, notice: "Import started. Check /admin/sidekiq to see when job is complete"
-
end
-
-
collection_action :add_admin, method: :post do
-
Membership.find(params[:membership_id]).update(admin: true)
-
redirect_to admin_group_path(Group.find(params[:group_id]))
-
end
-
-
collection_action :remove_admin, method: :post do
-
Membership.find(params[:membership_id]).update(admin: false)
-
redirect_to admin_group_path(Group.find(params[:group_id]))
-
end
-
-
member_action :move, method: :post do
-
group = Group.friendly.find(params[:id])
-
parent = Group.friendly.find(params[:parent_id])
-
GroupService.move(group: group, parent: parent, actor: current_user)
-
redirect_to admin_group_path(group)
-
end
-
-
member_action :handle, method: :post do
-
params.permit(:id, :handle)
-
group = Group.friendly.find(params[:id])
-
group.update(handle: params[:handle])
-
redirect_to admin_group_path(group)
-
end
-
-
member_action :archive, :method => :post do
-
group = Group.friendly.find(params[:id])
-
group.archive!
-
flash[:notice] = "Archived #{group.name}"
-
redirect_to [:admin, :groups]
-
end
-
-
member_action :unarchive, :method => :post do
-
group = Group.friendly.find(params[:id])
-
group.unarchive!
-
flash[:notice] = "Unarchived #{group.name}"
-
redirect_to [:admin, :groups]
-
end
-
-
member_action :delete_group, :method => :post do
-
GroupService.destroy_without_warning!(params[:id])
-
redirect_to [:admin, :groups]
-
end
-
-
member_action :export_group, method: :post do
-
group = Group.friendly.find(params[:id])
-
GroupExportWorker.perform_async(group.all_groups.pluck(:id), group.name, current_user.id)
-
redirect_to admin_group_path(group)
-
end
-
-
collection_action :export_users, method: :get do
-
render 'export_users'
-
end
-
-
collection_action :export_users_report, method: :get do
-
@users = User.joins(:memberships).
-
where(:'memberships.group_id' => params[:group_ids].split(' ').map(&:to_i))
-
if params[:coordinators]
-
@users = @users.where(:'memberships.admin' => true)
-
end
-
render 'export_users_report'
-
end
-
-
end
-
ActiveAdmin.register Subscription do
-
includes :groups
-
actions :new, :create, :index, :show, :edit, :destroy
-
-
filter :chargify_subscription_id
-
filter :expires_at, as: :date_range
-
filter :payment_method, as: :select
-
filter :plan, as: :select
-
filter :state, as: :select
-
-
index do
-
column :plan
-
column 'Groups' do |subscription|
-
if subscription.groups.any?
-
subscription.groups.map { |group| link_to(group.name, admin_group_path(group)) }
-
else
-
nil
-
end
-
end
-
column :state
-
column :expires_at
-
column :payment_method
-
column :chargify_subscription_id
-
column :owner
-
actions
-
end
-
-
show do
-
attributes_table do
-
row :id
-
row :plan
-
row :state
-
row :expires_at
-
row :chargify_subscription_id do |subscription|
-
if subscription.chargify_subscription_id
-
link_to subscription.chargify_subscription_id, "http://#{ENV['CHARGIFY_APP_NAME']}.chargify.com/subscriptions/#{subscription.chargify_subscription_id}", target: '_blank'
-
end
-
end
-
row :payment_method
-
row :owner
-
row :groups do |subscription|
-
subscription.groups.map do |group|
-
link_to group.name, admin_group_path(group.id)
-
end.join(', ').html_safe
-
end
-
row :max_threads
-
row :max_members
-
row :max_orgs
-
row :allow_guests
-
row :allow_subgroups
-
row :info
-
end
-
-
panel("Refresh chargify") do
-
if subscription.chargify_subscription_id
-
form action: refresh_admin_subscription_path(subscription), method: :post do |f|
-
f.input type: :hidden, name: :authenticity_token
-
f.input type: :submit, value: "refresh chargify"
-
end
-
else
-
"no chargify subscription to refresh"
-
end
-
end
-
end
-
-
form do |f|
-
inputs 'Subscription' do
-
input :plan, as: :select, collection: SubscriptionService::PLANS.keys
-
input :payment_method, as: :select, collection: Subscription::PAYMENT_METHODS
-
input :state, as: :select, collection: ['active', 'on_hold', 'pending', 'past_due', 'canceled']
-
input :expires_at
-
input :max_threads
-
input :max_members
-
input :max_orgs
-
input :allow_guests
-
input :allow_subgroups
-
input :chargify_subscription_id, label: "Chargify Subscription Id"
-
input :owner_id, label: "Owner Id"
-
end
-
f.actions
-
end
-
-
member_action :update, :method => :put do
-
subscription = Subscription.find(params[:id])
-
subscription.update(permitted_params[:subscription])
-
redirect_to admin_subscriptions_path, notice: "subscription updated"
-
end
-
-
member_action :refresh, :method => :post do
-
subscription = Subscription.find(params[:id])
-
SubscriptionService.update(subscription: subscription,
-
params: SubscriptionService.chargify_get(subscription.chargify_subscription_id))
-
redirect_to [:admin, subscription]
-
end
-
-
controller do
-
def permitted_params
-
params.permit!
-
end
-
end
-
end
-
ActiveAdmin.register User do
-
actions :index, :show, :edit
-
-
filter :name
-
filter :username
-
filter :email, as: :string
-
filter :email_newsletter
-
filter :email_verified
-
filter :sign_in_count
-
filter :detected_locale, as: :string
-
filter :time_zone
-
filter :created_at
-
-
scope :all
-
scope :coordinators
-
-
csv do
-
column :name
-
column :email
-
column :email_newsletter
-
column :locale
-
column :time_zone
-
end
-
-
controller do
-
def permitted_params
-
params.permit!
-
end
-
-
def find_resource
-
User.friendly.find(params[:id])
-
end
-
end
-
-
index do
-
column :name
-
column :email
-
column :created_at
-
column :last_sign_in_at
-
column "No. of groups", :memberships_count
-
column :deactivated_at
-
column :email_verified
-
column :locale
-
column :time_zone
-
column :bot
-
actions
-
end
-
-
form do |f|
-
f.inputs "Details" do
-
f.input :name
-
f.input :email, as: :string
-
f.input :username, as: :string
-
f.input :is_admin
-
f.input :bot, label: 'Bot account (do not add to polls)'
-
end
-
f.actions
-
end
-
-
member_action :update, :method => :put do
-
user = User.friendly.find(params[:id])
-
user.name = params[:user][:name]
-
user.email = params[:user][:email]
-
user.username = params[:user][:username]
-
user.is_admin = params[:user][:is_admin]
-
user.bot = params[:user][:bot]
-
user.save
-
redirect_to admin_users_path, :notice => "User updated"
-
end
-
-
member_action :login_as, :method => :get do
-
@user = User.friendly.find(params[:id])
-
@token = @user.login_tokens.create
-
end
-
-
member_action :merge, method: :post do
-
source = User.friendly.find(params[:id])
-
destination = User.find_by!(email: params[:destination_email].strip)
-
MigrateUserWorker.perform_async(source.id, destination.id)
-
redirect_to admin_user_path(destination)
-
end
-
-
member_action :redact, method: :put do
-
RedactUserWorker.perform_async(params[:id].to_i, current_user.id)
-
redirect_to admin_users_path, :notice => "User scheduled for deletion immediately"
-
end
-
-
member_action :deactivate, method: :put do
-
DeactivateUserWorker.perform_async(params[:id].to_i, current_user.id)
-
redirect_to admin_users_path, :notice => "User scheduled for deactivation immediately"
-
end
-
-
member_action :reactivate, method: :put do
-
GenericWorker.perform_async('UserService', 'reactivate', params[:id].to_i)
-
redirect_to admin_users_path, :notice => "User scheduled for reactivation immediately"
-
end
-
-
member_action :delete_spam, method: :delete do
-
DestroyUserWorker.perform_async(params[:id].to_i)
-
redirect_to admin_users_path, :notice => "User scheduled for spam deletion immediately"
-
end
-
-
member_action :delete_identity, method: :post do
-
User.find(params[:id]).identities.find(params[:identity_id]).destroy
-
redirect_to admin_user_path(User.find(params[:id]))
-
end
-
-
show do |user|
-
attributes_table do
-
user.attributes.each do |k,v|
-
row k.to_sym
-
end
-
end
-
-
if !user.deactivated_at
-
panel("Deactivate") do
-
button_to 'Deactivate User', deactivate_admin_user_path(user.id), method: :put, data: {confirm: 'Are you sure you want to deactivate this user? (this is reversable, user is not notified)'}
-
end
-
end
-
-
if user.deactivated_at && user.name.present?
-
panel("Reactivate") do
-
button_to 'Reactivate User', reactivate_admin_user_path(user.id), method: :put, data: {confirm: 'Are you sure you want to reactivate this user? (user is not notified)'}
-
end
-
end
-
-
if !user.email.nil?
-
panel("Deactivate and Redact (delete personally identifying information)") do
-
button_to 'Redact user', redact_admin_user_path(user.id), method: :put, data: {confirm: 'Are you sure you want to redact this user? (this is permanent, the user will be notified by email)'}
-
end
-
end
-
-
panel("Delete spam account") do
-
[
-
p("Delete the user and any groups, threads, comments, votes they created. It will not groups or threads they are simply a member of."),
-
button_to('Destroy User', delete_spam_admin_user_path(user.id), method: :delete, data: {confirm: 'Are you sure you want to destroy this user and content they authored?'})
-
].join.html_safe
-
end
-
-
panel("Memberships") do
-
table_for user.all_memberships.includes(:group, :user).order(:id).each do |m|
-
column :id
-
column :group_name do |g|
-
group = g.group
-
link_to group.full_name, admin_group_path(group)
-
end
-
column :volume
-
column :admin
-
column :accepted_at
-
column :revoked_at
-
end
-
end
-
-
render 'notifications', { notifications: Notification.includes(:event).where(user_id: user.id).order("id DESC").limit(30) }
-
-
panel("Identities") do
-
table_for user.identities.each do |ui|
-
column :id
-
column :identity_type
-
column :uid
-
column :name
-
column :email
-
column :destroy do |uii|
-
button_to 'delete', delete_identity_admin_user_path(ui.user), method: :post, params: {identity_id: uii.id}
-
end
-
end
-
end
-
-
panel 'Merge into another user' do
-
form action: merge_admin_user_path(user), method: :post do |f|
-
f.input type: :hidden, name: :authenticity_token
-
f.label "Email address of final user account"
-
f.input name: :destination_email
-
f.input type: :submit, value: "Merge user"
-
end
-
end
-
-
panel 'login as user' do
-
a(href: login_as_admin_user_path(user), target: "_blank") do
-
"Login as #{user.name}"
-
end
-
end
-
end
-
end
-
1
class API::B2::BaseController < API::V1::SnorlaxBase
-
1
skip_before_action :verify_authenticity_token
-
1
before_action :authenticate_api_key!
-
1
include ::LoadAndAuthorize
-
-
1
def authenticate_api_key!
-
25
raise CanCan::AccessDenied unless current_user
-
end
-
-
1
def current_user
-
84
@current_user ||= User.active.find_by(api_key: params[:api_key])
-
end
-
-
1
private
-
1
def permitted_params
-
10
jarams = params.dup
-
10
if jarams[:api_key]
-
10
jarams.delete(:api_key)
-
10
jarams.delete(:format)
-
10
jarams.delete(:discussion)
-
10
jarams.delete(:poll)
-
10
jarams = ActionController::Parameters.new({resource_name => jarams})
-
end
-
-
10
@permitted_params ||= PermittedParams.new(jarams)
-
end
-
end
-
1
class API::B2::DiscussionsController < API::B2::BaseController
-
1
def show
-
self.resource = load_and_authorize(:discussion)
-
respond_with_resource
-
end
-
-
1
def create
-
4
instantiate_resource
-
4
DiscussionService.create(actor: current_user, discussion: @discussion, params: params)
-
2
respond_with_resource
-
end
-
end
-
1
class API::B2::MembershipsController < API::B2::BaseController
-
1
def create
-
8
current_emails = User.active.where(id: group.memberships.pluck(:user_id)).pluck(:email)
-
-
4
params_emails = params.fetch(:emails, [])
-
4
add_emails = params_emails - current_emails
-
4
remove_emails = current_emails - params_emails
-
-
4
self.collection = GroupService.invite(
-
group: group,
-
actor: current_user,
-
params: {recipient_emails: add_emails}
-
)
-
4
PollService.group_members_added(group.id)
-
-
4
removed_user_ids = []
-
4
if params[:remove_absent].to_i == 1
-
2
Membership.where(
-
group_id: group.id,
-
user_id: User.where(email: remove_emails).pluck(:id)
-
).each do |membership|
-
2
removed_user_ids << membership.user_id
-
2
MembershipService.revoke(
-
membership: membership,
-
actor: current_user
-
)
-
end
-
end
-
-
4
render json: {
-
added_emails: User.where(id: collection.pluck(:user_id)).pluck(:email),
-
removed_emails: User.where(id: removed_user_ids).pluck(:email)
-
}
-
end
-
-
1
def index
-
instantiate_collection
-
respond_with_collection
-
end
-
-
1
def accessible_records
-
Membership.where(group_id: group.id)
-
end
-
-
1
def group
-
18
group = Group.find(params[:group_id])
-
17
raise ActiveRecord::RecordNotFound, "Group not found" unless group
-
-
17
unless current_user.is_admin? || current_user.adminable_groups.include?(group)
-
3
raise CanCan::AccessDenied, "User is not an admin"
-
end
-
14
group
-
end
-
-
1
def default_scope
-
super.merge(include_email: true)
-
end
-
end
-
1
class API::B2::PollsController < API::B2::BaseController
-
1
def show
-
self.resource = load_and_authorize(:poll)
-
respond_with_resource
-
end
-
-
1
def create
-
6
instantiate_resource
-
6
if PollService.create(actor: current_user, poll: @poll, params: params)
-
4
PollService.invite(actor: current_user, poll: @poll, params: params)
-
4
respond_with_resource
-
else
-
1
respond_with_errors
-
end
-
end
-
end
-
1
class API::B3::UsersController < API::V1::SnorlaxBase
-
1
skip_before_action :verify_authenticity_token
-
1
before_action :authenticate_api_key!
-
1
include ::LoadAndAuthorize
-
-
1
def authenticate_api_key!
-
7
raise CanCan::AccessDenied unless ENV.fetch('B3_API_KEY', '').length > 16
-
7
raise CanCan::AccessDenied unless params[:b3_api_key] == ENV['B3_API_KEY']
-
end
-
-
1
def deactivate
-
2
user = User.active.find(params[:id]) # throws 404 if not present
-
1
DeactivateUserWorker.perform_async(user.id, user.id)
-
1
render json: {success: :ok}
-
end
-
-
1
def reactivate
-
2
User.deactivated.find(params[:id]) # throws 404 if not present
-
1
UserService.reactivate(params[:id])
-
1
render json: {success: :ok}
-
end
-
end
-
1
class API::V1::AnnouncementsController < API::V1::RestfulController
-
1
def audience
-
current_user.ability.authorize! :show, target_model
-
-
if target_model.respond_to?(:anonymous) &&
-
target_model.anonymous &&
-
['decided_voters', 'undecided_voters'].include?(params[:recipient_audience])
-
raise CanCan::AccessDenied
-
end
-
-
self.collection = AnnouncementService.audience_users(
-
target_model,
-
params[:recipient_audience],
-
current_user,
-
params[:exclude_members],
-
params[:include_actor].present?
-
)
-
respond_with_collection
-
end
-
-
1
def new_member_count
-
current_user.ability.authorize! :show, target_model
-
-
count = UserInviter.new_members_count(
-
parent_group: target_model.parent_or_self,
-
user_ids: String(params[:recipient_user_xids]).split('x').map(&:to_i),
-
emails: String(params[:recipient_emails_cmr]).split(',')
-
)
-
render json: {count: count}
-
end
-
-
# count for number of notifications that will be send
-
1
def count
-
2
count = UserInviter.count(
-
actor: current_user,
-
model: target_model,
-
emails: String(params[:recipient_emails_cmr]).split(','),
-
user_ids: String(params[:recipient_user_xids]).split('x').map(&:to_i),
-
chatbot_ids: String(params[:recipient_chatbot_xids]).split('x').map(&:to_i),
-
audience: params[:recipient_audience],
-
usernames: String(params[:recipient_usernames]).split(','),
-
exclude_members: params[:exclude_members].present?,
-
include_actor: params[:include_actor].present?
-
)
-
2
render json: {count: count}
-
end
-
-
1
def search
-
# if target model has no groups, no discussions, then draw from users groups and guest threads
-
self.collection = if params[:existing_only]
-
target_model.members.search_for(params[:q]).limit(50)
-
else
-
UserQuery.invitable_search(
-
model: target_model,
-
actor: current_user,
-
q: params[:q]
-
)
-
end
-
respond_with_collection serializer: AuthorSerializer, root: :users
-
end
-
-
1
def create
-
52
if target_model.is_a?(Group)
-
10
self.collection = GroupService.invite(group: target_model, actor: current_user, params: params)
-
8
respond_with_collection serializer: MembershipSerializer, root: :memberships
-
41
elsif target_model.is_a?(Discussion)
-
9
event = DiscussionService.invite(discussion: target_model, actor: current_user, params: params)
-
7
self.collection = DiscussionReader.where(discussion_id: target_model.id, user_id: event.recipient_user_ids)
-
7
respond_with_collection serializer: DiscussionReaderSerializer, root: :discussion_readers
-
32
elsif target_model.is_a?(Poll)
-
15
self.collection = PollService.invite(poll: target_model, actor: current_user, params: params)
-
13
respond_with_collection serializer: StanceSerializer, root: :stances
-
17
elsif target_model.is_a?(Outcome)
-
17
self.collection = OutcomeService.invite(outcome: target_model, actor: current_user, params: params)
-
15
respond_with_collection serializer: UserSerializer, root: :users
-
end
-
end
-
-
1
def users_notified_count
-
# returns a count of users notified about this thing
-
current_user.ability.authorize! :show, target_model
-
-
count = Notification.
-
joins(:event).
-
where("events.id": target_event_ids).
-
count("DISTINCT notifications.user_id")
-
-
render json: {count: count}
-
end
-
-
1
def history
-
1
notifications = {}
-
-
1
events = Event.where(kind: notification_kinds, id: target_event_ids).order('id desc').limit(1000)
-
-
1
allow_viewed = true
-
-
1
if target_model.respond_to?(:discussion) &&
-
target_model.discussion.present? &&
-
target_model.discussion.polls.kept.where(anonymous: true).any?
-
allow_viewed = false
-
end
-
-
1
if target_model.respond_to?(:poll) &&
-
target_model.poll.present? &&
-
target_model.poll.anonymous?
-
allow_viewed = false
-
end
-
-
1
Notification.includes(:user).where(event_id: events.pluck(:id)).order('users.name, users.email').each do |notification|
-
1
next unless notification.user
-
1
notifications[notification.event_id] = [] unless notifications.has_key?(notification.event_id)
-
1
notifications[notification.event_id] << {id: notification.id, to: (notification.user.name || notification.user.email), viewed: allow_viewed && notification.viewed }
-
end
-
-
1
res = events.map do |event|
-
1
{event_id: event.id,
-
created_at: event.created_at,
-
author_name: event.user.name,
-
kind: event.kind,
-
notifications: notifications[event.id] || [] }
-
1
end.filter {|e| e[:notifications].size > 0}
-
-
1
render root: false, json: {allow_viewed: allow_viewed, data: res}
-
end
-
-
1
private
-
-
1
def target_event_ids
-
1
if target_model.is_a?(Discussion)
-
polls = Poll.where(discussion_id: target_model.id)
-
outcomes = Outcome.where(poll_id: polls.map(&:id))
-
comments = Comment.where(discussion_id: target_model.id)
-
eventables = [target_model, polls, outcomes, comments].flatten.compact
-
else
-
1
eventables = [target_model]
-
end
-
-
1
event_ids = Event.where(kind: notification_kinds, eventable: eventables).pluck(:id)
-
end
-
-
1
def notification_kinds
-
2
%w[announcement_created
-
user_mentioned
-
announcement_resend
-
discussion_announced
-
poll_announced
-
outcome_announced
-
outcome_created
-
outcome_updated
-
outcome_edited
-
poll_created
-
poll_edited
-
poll_reminder
-
new_discussion
-
discussion_edited
-
comment_replied_to
-
poll_closing_soon]
-
end
-
-
1
def default_scope
-
43
if target_model && target_model.respond_to?(:group_id)
-
43
is_admin = target_model.group_id ? target_model.group.admins.exists?(current_user.id) : target_model.admins.exists?(current_user.id)
-
else
-
is_admin = false
-
end
-
-
43
super.merge(
-
43
include_email: (is_admin)
-
)
-
end
-
-
1
def authorize_model
-
load_and_authorize(:group, :announce, optional: true) ||
-
load_and_authorize(:discussion, :announce, optional: true) ||
-
load_and_authorize(:poll, :announce, optional: true) ||
-
load_and_authorize(:outcome, :announce, optional: false)
-
end
-
-
1
def target_model
-
379
load_and_authorize(:group, :show, optional: true) ||
-
load_and_authorize(:discussion, :show, optional: true) ||
-
load_and_authorize(:comment, :show, optional: true) ||
-
load_and_authorize(:poll, :show, optional: true) ||
-
load_and_authorize(:outcome, :show, optional: true)
-
end
-
end
-
1
class API::V1::AttachmentsController < API::V1::RestfulController
-
1
def index
-
# Group.find_by(params[:group_id).id_and_subgroup_ids
-
1
group = current_user.groups.find_by!(id: params[:group_id])
-
1
group_ids = current_user.group_ids.intersection(group.id_and_subgroup_ids)
-
1
self.collection = AttachmentQuery.find(group_ids, params[:q], (params[:per] || 20), (params[:from] || 0))
-
1
self.collection_count = AttachmentQuery.find(group_ids, params[:q], 1000, 0).count
-
1
respond_with_collection
-
end
-
-
1
def destroy
-
2
attachment = load_and_authorize :attachment, :destroy
-
1
record = attachment.record
-
1
attachment.purge_later
-
1
record.save!
-
1
serializer = "#{record.class.to_s}Serializer".constantize
-
1
root = record.class.to_s.pluralize.underscore
-
1
self.collection = [record]
-
1
render json: resources_to_serialize, scope: default_scope, each_serializer: serializer, root: root
-
end
-
-
1
def serializer_root
-
:attachments
-
end
-
-
1
def serializer_class
-
1
AttachmentSerializer
-
end
-
-
1
def accessible_records
-
AttachmentQuery
-
end
-
-
1
def serializer_root
-
1
'attachments'
-
end
-
end
-
class API::V1::BootController < API::V1::RestfulController
-
def site
-
render json: Boot::Site.new.payload.merge(user_payload)
-
EventBus.broadcast('boot_site', current_user)
-
end
-
-
def version
-
render json: {
-
version: Loomio::Version.current,
-
release: AppConfig.release,
-
reload: (params.fetch(:version, '0.0.0') < Loomio::Version.current) ||
-
(ENV['LOOMIO_SYSTEM_RELOAD'] && AppConfig.release != params[:release]),
-
notice: ENV['LOOMIO_SYSTEM_NOTICE']
-
}
-
end
-
-
private
-
def user_payload
-
Boot::User.new(current_user,
-
identity: serialized_pending_identity,
-
flash: flash,
-
channel_token: set_channel_token).payload
-
end
-
-
def set_channel_token
-
token = SecureRandom.hex
-
CACHE_REDIS_POOL.with do |client|
-
client.set("/current_users/#{token}",
-
{name: current_user.name,
-
group_ids: current_user.group_ids,
-
id: current_user.id}.to_json)
-
end
-
token
-
end
-
-
def current_user
-
restricted_user || super
-
end
-
end
-
class API::V1::ChatbotsController < API::V1::RestfulController
-
def index
-
load_and_authorize(:group, :show_chatbots)
-
self.collection = Chatbot.where(group_id: @group.id)
-
respond_with_collection(scope: index_scope)
-
end
-
-
def test
-
ChatbotService.publish_test!(params)
-
head :ok
-
end
-
-
def index_scope
-
default_scope.merge({ current_user_is_admin: @group.admins.exists?(current_user.id)})
-
end
-
end
-
1
class API::V1::CommentsController < API::V1::RestfulController
-
1
def discard
-
2
load_resource
-
2
@event = service.discard(comment: resource, actor: current_user)
-
1
respond_with_resource(scope: default_scope.merge(exclude_types: %w[discussion group user]))
-
end
-
-
1
def undiscard
-
load_resource
-
@event = service.undiscard(comment: resource, actor: current_user)
-
respond_with_resource(scope: {exclude_types: %w[discussion group user]})
-
end
-
-
1
def destroy
-
load_resource
-
@event = @comment.created_event.parent
-
destroy_action
-
@event.reload
-
render json: MessageChannelService.serialize_models(@event.children.compact, scope: default_scope)
-
end
-
end
-
class API::V1::ContactMessagesController < API::V1::RestfulController
-
end
-
class API::V1::DemosController < API::V1::RestfulController
-
before_action :require_current_user, only: [:clone]
-
-
def index
-
instantiate_collection
-
respond_with_collection
-
end
-
-
def clone
-
group = DemoService.take_demo(current_user)
-
GenericWorker.perform_async('DemoService', 'refill_queue')
-
self.collection = [group]
-
respond_with_collection
-
end
-
-
def accessible_records
-
Demo.all
-
end
-
end
-
class API::V1::DiscussionReadersController < API::V1::RestfulController
-
def index
-
@discussion = load_and_authorize(:discussion)
-
query = params[:query]
-
instantiate_collection do |collection|
-
collection = collection.where(discussion_id: @discussion.id)
-
if query
-
collection = collection.
-
joins('LEFT OUTER JOIN users on discussion_readers.user_id = users.id').
-
where("users.name ilike :first OR
-
users.name ilike :last OR
-
users.email ilike :first OR
-
users.username ilike :first",
-
first: "#{query}%", last: "% #{query}%")
-
end
-
collection
-
end
-
respond_with_collection
-
end
-
-
def make_admin
-
current_user.ability.authorize! :make_admin, discussion_reader
-
discussion_reader.update(admin: true)
-
respond_with_resource
-
end
-
-
def remove_admin
-
current_user.ability.authorize! :remove_admin, discussion_reader
-
discussion_reader.update(admin: false)
-
respond_with_resource
-
end
-
-
def resend
-
current_user.ability.authorize! :resend, discussion_reader
-
raise NotImplementedError.new
-
end
-
-
def revoke
-
current_user.ability.authorize! :remove, discussion_reader
-
discussion_reader.update(revoked_at: Time.zone.now, revoker_id: current_user.id)
-
respond_with_resource
-
end
-
-
private
-
-
def discussion_reader
-
@discussion_reader = DiscussionReader.find(params[:id])
-
end
-
-
def default_scope
-
discussion = (@discussion_reader || @discussion).discussion
-
is_admin = discussion.group_id ? discussion.group.admins.exists?(current_user.id) : discussion.admins.exists?(current_user.id)
-
super.merge({include_email: is_admin})
-
end
-
-
def accessible_records
-
DiscussionReader.includes(:user, :discussion).where(discussion_id: @discussion.id)
-
end
-
end
-
class API::V1::DiscussionTemplatesController < API::V1::RestfulController
-
def browse_tags
-
tag_counts = {}
-
DiscussionTemplate.where(public: true).pluck(:tags).flatten.each do |tag|
-
tag_counts[tag] ||= 0
-
tag_counts[tag] += 1
-
end
-
render json: tag_counts.sort_by {|k,v| v}.to_h.keys.slice(0, 20), root: false
-
end
-
-
def browse
-
if DiscussionTemplate.where(public: true).count == 0
-
DiscussionTemplateService.create_public_templates
-
end
-
-
templates = DiscussionTemplate
-
.joins("LEFT JOIN groups ON groups.id = discussion_templates.group_id LEFT JOIN subscriptions ON groups.subscription_id = subscriptions.id")
-
.where("discussion_templates.public": true)
-
.where("groups.handle = ? OR subscriptions.plan != ?", 'templates', 'trial')
-
-
if params[:query].present?
-
templates = templates.where("process_name ILIKE :q OR process_subtitle ILIKE :q OR tags @> ARRAY[:a]::varchar[]", q: "%#{params[:query]}%", a: Array(params[:query]))
-
end
-
-
templates = templates.limit(50).to_a
-
-
authors = access_by_id(User.where(id: templates.map(&:author_id)))
-
groups = access_by_id(Group.where(id: templates.map(&:group_id)))
-
-
results = templates.map do |dt|
-
author = authors[dt.author_id]
-
group = groups[dt.group_id]
-
{
-
id: dt.id,
-
process_name: dt.process_name,
-
process_subtitle: dt.process_subtitle,
-
author_name: author&.name,
-
group_name: group&.name,
-
avatar_url: (group&.logo_url || author&.avatar_url),
-
tags: dt.tags
-
}
-
end
-
-
render json: results, root: :results
-
end
-
-
def index
-
group = current_user.groups.find_by(id: params[:group_id]) || NullGroup.new
-
-
if group.discussion_templates.kept.count == 0
-
group.discussion_templates = DiscussionTemplateService.initial_templates(group.category)
-
end
-
-
if params[:id]
-
self.collection = Array(DiscussionTemplate.find_by(group_id: current_user.group_ids, id: params[:id]))
-
else
-
self.collection = group.discussion_templates
-
end
-
-
respond_with_collection
-
end
-
-
def show
-
@discussion_template = DiscussionTemplate.where('group_id IN (?) OR public = true', current_user.group_ids).find(params[:id])
-
respond_with_resource
-
end
-
-
def positions
-
group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
-
params[:ids].each_with_index do |val, index|
-
if val.is_a? Integer
-
DiscussionTemplate.where(id: val, group_id: group.id).update_all(position: index)
-
else
-
group.discussion_template_positions[val] = index
-
end
-
end
-
-
group.save!
-
index
-
end
-
-
def discard
-
@group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
@discussion_template = @group.discussion_templates.kept.find_by!(id: params[:id])
-
@discussion_template.discard!
-
index
-
end
-
-
def undiscard
-
@group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
@discussion_template = @group.discussion_templates.discarded.find_by!(id: params[:id])
-
@discussion_template.undiscard!
-
index
-
end
-
-
def destroy
-
@discussion_template = DiscussionTemplate.find(params[:id])
-
current_user.adminable_groups.find(@discussion_template.group_id)
-
@discussion_template.destroy!
-
destroy_response
-
end
-
-
def hide
-
@group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
-
if DiscussionTemplateService.group_templates(group: @group).any? {|pt| pt.key == params[:key]}
-
@group = current_user.adminable_groups.find_by(id: params[:group_id])
-
@group.hidden_discussion_templates ||= []
-
@group.hidden_discussion_templates.push params[:key].parameterize
-
@group.hidden_discussion_templates.uniq!
-
@group.save!
-
index
-
else
-
response_with_error(404)
-
end
-
end
-
-
def unhide
-
@group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
-
if DiscussionTemplateService.group_templates(group: @group).any? {|pt| pt.key == params[:key]}
-
@group = current_user.adminable_groups.find_by(id: params[:group_id])
-
@group.hidden_discussion_templates -= [params[:key].parameterize]
-
@group.save!
-
self.resource = @group
-
index
-
else
-
response_with_error(404)
-
end
-
end
-
-
private
-
def access_by_id(collection, id_col = 'id')
-
h = {}
-
collection.each do |row|
-
h[row.send(id_col)] = row
-
end
-
h
-
end
-
-
end
-
1
class API::V1::DiscussionsController < API::V1::RestfulController
-
1
def create
-
16
instantiate_resource
-
15
if resource_params[:forked_event_ids] && resource_params[:forked_event_ids].any?
-
EventService.move_comments(discussion: create_action.discussion, params: resource_params, actor: current_user)
-
else
-
15
create_action
-
end
-
11
respond_with_resource
-
end
-
-
1
def create_action
-
15
@event = service.create(**{resource_symbol => resource, actor: current_user, params: resource_params})
-
end
-
-
1
def show
-
6
load_and_authorize(:discussion)
-
-
4
if resource.closed_at && resource.closer_id.nil?
-
if closed_event = Event.where(discussion_id: resource.id, kind: 'discussion_closed').order(:id).last
-
resource.update_attribute(:closer_id, closed_event.user_id)
-
else
-
resource.update_attribute(:closer_id, resource.author_id)
-
end
-
end
-
-
# this is desperation in code, but better than auto create when nil on method call
-
4
if resource.created_event.nil?
-
EventService.repair_thread(resource.id)
-
resource.reload
-
end
-
-
4
accept_pending_membership
-
4
respond_with_resource
-
end
-
-
1
def index
-
7
load_and_authorize(:group, optional: true)
-
7
instantiate_collection do |collection|
-
7
DiscussionQuery.filter(chain: collection, filter: params[:filter])
-
end
-
7
respond_with_collection
-
end
-
-
1
def dashboard
-
5
raise CanCan::AccessDenied.new unless current_user.is_logged_in?
-
4
@accessible_records = DiscussionQuery.dashboard(user: current_user)
-
8
instantiate_collection { |collection| collection.is_open.order_by_latest_activity }
-
4
respond_with_collection
-
end
-
-
1
def direct
-
@accessible_records = DiscussionQuery.visible_to(
-
user: current_user,
-
or_public: false,
-
or_subgroups: false,
-
group_ids: nil,
-
only_direct: true)
-
instantiate_collection { |collection| collection.order_by_latest_activity }
-
respond_with_collection
-
end
-
-
1
def inbox
-
5
raise CanCan::AccessDenied.new unless current_user.is_logged_in?
-
4
@accessible_records = DiscussionQuery.inbox(user: current_user)
-
8
instantiate_collection { |collection| collection.recent.order_by_latest_activity }
-
4
respond_with_collection
-
end
-
-
1
def search
-
load_and_authorize(:group)
-
instantiate_collection { |collection| collection.search_for(params.require(:q)) }
-
respond_with_collection
-
end
-
-
1
def move
-
1
@event = service.move discussion: load_resource, params: params, actor: current_user
-
1
respond_with_resource
-
end
-
-
1
def history
-
load_and_authorize(:discussion)
-
-
if @discussion.polls.kept.where(anonymous:true).any?
-
render root: false, json: {message: I18n.t("discussion_last_seen_by.disabled_anonymous_polls")}, status: 403
-
else
-
res = DiscussionReader.joins(:user).where(discussion: @discussion).where.not(last_read_at: nil).map do |reader|
-
{reader_id: reader.id,
-
last_read_at: reader.last_read_at,
-
user_name: reader.user.name_or_username }
-
end
-
render root: false, json: res
-
end
-
end
-
-
1
def mark_as_seen
-
2
service.mark_as_seen discussion: load_resource, actor: current_user
-
1
respond_ok
-
end
-
-
1
def mark_as_read
-
1
service.mark_as_read(discussion: load_resource, params: params, actor: current_user)
-
1
respond_ok
-
end
-
-
1
def dismiss
-
1
service.dismiss discussion: load_resource, params: params, actor: current_user
-
1
respond_with_resource
-
end
-
-
1
def recall
-
1
service.recall discussion: load_resource, params: params, actor: current_user
-
1
respond_with_resource
-
end
-
-
1
def close
-
3
@event = service.close discussion: load_resource, actor: current_user
-
1
respond_with_resource
-
end
-
-
1
def reopen
-
3
@event = service.reopen discussion: load_resource, actor: current_user
-
1
respond_with_resource
-
end
-
-
1
def move_comments
-
7
EventService.move_comments(discussion: load_resource, params: params, actor: current_user)
-
5
respond_with_resource
-
end
-
-
1
def pin
-
3
service.pin discussion: load_resource, actor: current_user
-
1
respond_with_resource
-
end
-
-
1
def unpin
-
1
service.unpin discussion: load_resource, actor: current_user
-
1
respond_with_resource
-
end
-
-
1
def set_volume
-
2
update_reader volume: params[:volume]
-
end
-
-
1
def discard
-
@discussion = load_resource
-
service.discard discussion: @discussion, actor: current_user
-
respond_with_resource
-
end
-
-
1
private
-
1
def group_ids
-
7
case params[:subgroups]
-
when 'all'
-
Array(@group&.id_and_subgroup_ids)
-
when 'mine'
-
if current_user.is_logged_in?
-
[@group&.id].concat(current_user.group_ids & @group&.id_and_subgroup_ids)
-
else
-
[@group&.id]
-
end
-
else
-
7
[@group&.id]
-
end.compact
-
end
-
-
1
def discussion_ids
-
7
params.fetch(:xids, '').split('x').map(&:to_i).uniq
-
end
-
-
1
def split_tags
-
7
Array(params[:tags].to_s).reject(&:blank?)
-
end
-
-
1
def accessible_records
-
15
@accessible_records ||= DiscussionQuery.visible_to(
-
user: current_user,
-
group_ids: group_ids,
-
tags: split_tags,
-
discussion_ids: discussion_ids)
-
end
-
-
1
def update_reader(params = {})
-
2
service.update_reader discussion: load_resource, params: params, actor: current_user
-
1
respond_with_resource
-
end
-
end
-
1
class API::V1::DocumentsController < API::V1::RestfulController
-
-
1
def for_group
-
7
self.collection = page_collection(for_group_documents).search_for(params[:q])
-
5
cache = RecordCache.for_collection(collection, current_user, exclude_types)
-
5
respond_with_collection scope: { group_id: @group.id, cache: cache }, serializer: DocumentSerializer
-
end
-
-
1
def for_discussion
-
2
load_and_authorize(:discussion)
-
1
self.collection = Queries::UnionQuery.for(:documents, [
-
@discussion.documents,
-
@discussion.poll_documents,
-
@discussion.comment_documents
-
])
-
1
respond_with_collection
-
end
-
-
1
private
-
-
1
def for_group_documents
-
7
if current_user.ability.can?(:see_private_content, load_and_authorize(:group))
-
5
private_group_documents
-
else
-
public_group_documents
-
end.order(created_at: :desc)
-
end
-
-
1
def private_group_documents
-
5
group_ids = case params[:subgroups]
-
when 'mine', 'all'
-
@group.id_and_subgroup_ids
-
else
-
5
Array(@group.id)
-
end
-
5
Document.where(group_id: group_ids)
-
end
-
-
1
def public_group_documents
-
Queries::UnionQuery.for(:documents, [
-
@group.documents,
-
@group.public_discussion_documents,
-
@group.public_comment_documents ])
-
-
end
-
-
1
def accessible_records
-
(
-
load_and_authorize(:group, optional: true) ||
-
load_and_authorize(:discussion, optional: true) ||
-
load_and_authorize(:comment, optional: true) ||
-
load_and_authorize(:poll, optional: true) ||
-
load_and_authorize(:outcome)
-
).documents
-
end
-
end
-
1
class API::V1::EventsController < API::V1::RestfulController
-
1
def position_keys
-
load_and_authorize(:discussion)
-
keys = Event.where(discussion_id: params[:discussion_id]).pluck(:position_key).sort
-
render json: keys, root: 'position_keys'
-
end
-
-
1
def timeline
-
load_and_authorize(:discussion)
-
data = Event.where(discussion_id: params[:discussion_id])
-
.order(:position_key)
-
.pluck(:position_key, :sequence_id, :created_at, :user_id, :depth, :descendant_count)
-
render json: data.to_json, root: 'timeline'
-
end
-
-
1
def remove_from_thread
-
service.remove_from_thread(event: load_resource, actor: current_user)
-
respond_with_resource
-
end
-
-
1
def comment
-
2
load_and_authorize(:discussion)
-
2
self.resource = Event.find_by!(kind: "new_comment", eventable_type: "Comment", eventable_id: params[:comment_id])
-
1
respond_with_resource
-
end
-
-
1
def pin
-
1
@event = Event.find(params[:id])
-
1
current_user.ability.authorize!(:pin, @event)
-
1
@event.update(pinned: true, pinned_title: params[:pinned_title])
-
1
render json: MessageChannelService.serialize_models(@event, scope: default_scope)
-
end
-
-
1
def unpin
-
@event = Event.find(params[:id])
-
current_user.ability.authorize!(:unpin, @event)
-
@event.update(pinned: false)
-
render json: MessageChannelService.serialize_models(@event, scope: default_scope)
-
end
-
-
1
private
-
-
1
def order
-
56
%w(sequence_id position position_key).detect {|col| col == params[:order] } || "sequence_id"
-
end
-
-
1
def per
-
7
(params[:per] || default_page_size).to_i
-
end
-
-
1
def from
-
7
if params[:from_sequence_id_of_position]
-
position = [params[:from_sequence_id_of_position].to_i, 1].max
-
Event.find_by!(discussion: @discussion, depth: 1, position: position)&.sequence_id
-
7
elsif params[:comment_id]
-
Event.find_by!(kind: "new_comment", eventable_type: "Comment", eventable_id: params[:comment_id])&.sequence_id
-
else
-
7
params[:from] || 0
-
end
-
end
-
-
1
def accessible_records
-
8
load_and_authorize(:discussion)
-
7
records = Event.where(discussion_id: @discussion.id)
-
-
7
if %w[position_key sequence_id].include?(params[:order_by])
-
records = records.order("#{params[:order_by]}#{params[:order_desc] ? " DESC" : ''}")
-
else
-
7
records = records.where("#{order} >= ?", from)
-
end
-
-
7
if params[:unread] == 'true'
-
reader = DiscussionReader.for(user: current_user, discussion: @discussion)
-
# could also be where in unread_ranges, but there is a bug on http://localhost:8080/s/njwV5RpS
-
records = records.where.not(sequence_id: reader.read_ranges.map{ |range| range[0]..range[1] })
-
end
-
-
7
if params[:pinned] == 'true'
-
records = records.where(pinned: true)
-
end
-
-
7
if params[:kind]
-
records = records.where("kind in (?)", params[:kind].split(','))
-
end
-
-
7
%w(parent_id depth sequence_id position position_key).each do |name|
-
35
records = records.where(name => params[name]) if params[name]
-
# records = records.where("#{name} >= ?", params["min_#{name}"]) if params["min_#{name}"]
-
# records = records.where("#{name} <= ?", params["max_#{name}"]) if params["max_#{name}"]
-
35
records = records.where("#{name} = ?", params["#{name}"]) if params["#{name}"]
-
35
records = records.where("#{name} < ?", params["#{name}_lt"]) if params["#{name}_lt"]
-
35
records = records.where("#{name} > ?", params["#{name}_gt"]) if params["#{name}_gt"]
-
35
records = records.where("#{name} <= ?", params["#{name}_lte"]) if params["#{name}_lte"]
-
35
records = records.where("#{name} >= ?", params["#{name}_gte"]) if params["#{name}_gte"]
-
35
records = records.where("#{name} like ?", params["#{name}_sw"]+"%") if params["#{name}_sw"]
-
end
-
# records = records.where("position_key like ?", params["position_key_sw"]+"%") if params["position_key_sw"]
-
7
records
-
end
-
-
1
def page_collection(collection)
-
7
if params[:until_sequence_id_of_position]
-
position = [params[:until_sequence_id_of_position].to_i, @discussion.created_event.child_count].min
-
event = Event.find_by!(discussion: @discussion, depth: 1, position: position)
-
max_sequence_id = event.sequence_id + event.child_count
-
collection.where("sequence_id <= ?", max_sequence_id).order('depth, position').limit(per)
-
else
-
7
collection.order(order).limit(per)
-
end
-
end
-
-
1
def default_page_size
-
5
30
-
end
-
end
-
class API::V1::GroupSurveysController < API::V1::RestfulController
-
-
def create
-
service.create(params: resource_params, actor: current_user)
-
render json: { success: :ok }
-
end
-
end
-
1
class API::V1::GroupsController < API::V1::RestfulController
-
1
def token
-
self.resource = load_and_authorize(:group, :invite_people)
-
respond_with_resource scope: {include_token: true, exclude_types: ['membership', 'user']}
-
end
-
-
1
def suggest_handle
-
3
render json: { handle: service.suggest_handle(name: params[:name], parent_handle: params[:parent_handle]) }
-
end
-
-
1
def reset_token
-
self.resource = load_and_authorize(:group, :invite_people)
-
resource.update(token: resource.class.generate_unique_secure_token)
-
respond_with_resource scope: {include_token: true, exclude_types: ['membership', 'user']}
-
end
-
-
1
def show
-
6
self.resource = load_and_authorize(:group)
-
5
accept_pending_membership
-
5
respond_with_resource
-
end
-
-
1
def index
-
1
ids = params.fetch(:xids, '').split('x').map(&:to_i)
-
1
if ids.length > 0
-
1
instantiate_collection do |collection|
-
1
collection = GroupQuery.visible_to(user: current_user, show_public: true).where(id: ids)
-
end
-
else
-
order_attributes = ['created_at', 'memberships_count']
-
order = (order_attributes.include? params[:order])? "groups.#{params[:order]} DESC" : 'groups.memberships_count DESC'
-
instantiate_collection { |collection| collection.search_for(params[:q]).order(order) }
-
end
-
1
respond_with_collection
-
end
-
-
1
def count_explore_results
-
1
render json: { count: Queries::ExploreGroups.new.search_for(params[:q]).count }
-
end
-
-
1
def subgroups
-
self.collection = load_and_authorize(:group).subgroups.select { |g| current_user.can? :show, g }
-
respond_with_collection
-
end
-
-
1
def upload_photo
-
ensure_photo_params
-
service.update group: load_resource, actor: current_user, params: { params[:kind] => params[:file] }
-
respond_with_resource
-
end
-
-
1
def export #json
-
2
service.export(group: load_and_authorize(:group, :export), actor: current_user)
-
1
render json: { success: :ok }
-
end
-
-
1
def export_csv
-
group = load_and_authorize(:group, :export)
-
GroupExportCsvWorker.perform_async(group.id, current_user.id)
-
render json: { success: :ok }
-
end
-
-
1
private
-
-
1
def ensure_photo_params
-
params.require(:file)
-
raise ActionController::UnpermittedParameters.new([:kind]) unless ['logo', 'cover_photo'].include? params.require(:kind)
-
end
-
-
1
def accessible_records
-
1
Queries::ExploreGroups.new
-
end
-
end
-
class API::V1::IdentitiesController < API::V1::RestfulController
-
ACTION_NAMES = %w(channels admin_groups)
-
-
def command
-
current_user.ability.authorize! :show, identity
-
if valid_command?
-
render json: api_response.json, root: false
-
else
-
render json: { error: "#{params[:command]} is invalid for this identity" }, status: :bad_request
-
end
-
end
-
-
private
-
-
def valid_command?
-
ACTION_NAMES.include?(params[:command]) && identity.respond_to?(params[:command])
-
end
-
-
def identity
-
@identity ||= Identities::Base.find(params[:id])
-
end
-
-
def api_response
-
@api_response ||= identity.send(params[:command])
-
end
-
end
-
class API::V1::LinkPreviewsController < API::V1::RestfulController
-
def create
-
# require logged in user
-
# add rate limit of 100 per hour per user
-
previews = LinkPreviewService.fetch_urls(filtered_urls)
-
render json: {previews: previews}
-
end
-
-
private
-
def filtered_urls
-
known_urls = []
-
if d = Discussion.find_by(id: params[:discussion_id])
-
known_urls = DiscussionService.extract_link_preview_urls(d)
-
end
-
params[:urls].reject {|url| known_urls.include?(url) }
-
end
-
end
-
1
class API::V1::LoginTokensController < API::V1::RestfulController
-
1
def create
-
4
save_detected_locale(login_token_user)
-
2
service.create(actor: login_token_user, uri: URI::parse(request.referrer.to_s))
-
2
render json: { success: :ok }
-
end
-
-
1
private
-
1
def login_token_user
-
6
User.find_by!(email: params.require(:email))
-
end
-
end
-
1
class API::V1::MembershipRequestsController < API::V1::RestfulController
-
-
1
before_action :authorize, only: [:pending, :previous]
-
-
1
def pending
-
1
@membership_requests = page_collection(@group.membership_requests.pending)
-
1
respond_with_collection
-
end
-
-
1
def my_pending
-
load_and_authorize :group
-
@membership_requests = @group.membership_requests.pending.where(requestor_id: current_user.id)
-
respond_with_collection
-
end
-
-
1
def previous
-
1
@membership_requests = page_collection(@group.membership_requests.responded_to)
-
1
respond_with_collection
-
end
-
-
1
def approve
-
2
service.approve(membership_request: load_resource, actor: current_user)
-
1
respond_with_resource
-
end
-
-
1
def ignore
-
2
service.ignore(membership_request: load_resource, actor: current_user)
-
1
respond_with_resource
-
end
-
-
1
private
-
-
1
def authorize
-
4
load_and_authorize :group
-
4
current_user.ability.authorize! :manage_membership_requests, @group
-
end
-
end
-
1
class API::V1::MembershipsController < API::V1::RestfulController
-
1
load_resource only: [:set_volume]
-
-
1
def index
-
5
instantiate_collection do |collection|
-
5
%w[user_xids].each do |key|
-
5
next unless params.has_key? key
-
params[key.gsub("_xids", "_ids")] = params[key].split('x').map(&:to_i)
-
params.delete(key)
-
end
-
5
MembershipQuery.search(chain: collection, params: params).order('memberships.group_id, memberships.admin desc, memberships.created_at desc')
-
end
-
5
respond_with_collection(scope: index_scope)
-
end
-
-
1
def destroy_response
-
render json: Array(resource.group), each_serializer: GroupSerializer, root: :groups, scope: {}
-
end
-
-
# move to profile controller later
-
1
def for_user
-
1
load_and_authorize :user
-
1
same_group_ids = current_user.group_ids & @user.group_ids
-
1
public_group_ids = @user.groups.where(listed_in_explore: true).pluck(:id)
-
1
instantiate_collection do |collection|
-
1
Membership.joins(:group).where(group_id: same_group_ids + public_group_ids, user_id: @user.id).active.order('groups.full_name')
-
end
-
1
respond_with_collection serializer: MembershipSerializer
-
end
-
-
1
def join_group
-
event = service.join_group group: load_and_authorize(:group), actor: current_user
-
@membership = event.eventable
-
respond_with_resource
-
end
-
-
1
def my_memberships
-
@memberships = current_user.memberships.includes(:user, :inviter)
-
respond_with_collection
-
end
-
-
1
def resend
-
3
service.resend membership: load_resource, actor: current_user
-
1
respond_with_resource
-
end
-
-
1
def make_admin
-
service.make_admin(membership: load_resource, actor: current_user)
-
respond_with_resource
-
end
-
-
1
def remove_admin
-
service.remove_admin(membership: load_resource, actor: current_user)
-
respond_with_resource
-
end
-
-
1
def set_volume
-
3
service.set_volume membership: resource, params: params.slice(:volume, :apply_to_all), actor: current_user
-
3
respond_with_resource
-
end
-
-
1
def save_experience
-
2
raise ActionController::ParameterMissing.new(:experience) unless params[:experience]
-
2
service.save_experience membership: load_resource, actor: current_user, params: { experience: params[:experience] }
-
1
respond_with_resource
-
end
-
-
1
def user_name
-
6
user = User.active.find(params[:id])
-
6
if (user.name.blank? || !user.email_verified) &&
-
5
(user.group_ids & current_user.adminable_group_ids).length > 0
-
3
user.update(name: params[:name], username: params[:username])
-
3
self.resource = user
-
3
respond_with_resource
-
else
-
3
error_response(403)
-
end
-
end
-
-
1
private
-
1
def destroy_action
-
service.revoke(**{resource_symbol => resource, actor: current_user})
-
end
-
-
1
def valid_orders
-
6
['memberships.created_at', 'memberships.created_at desc', 'users.name', 'admin desc', 'accepted_at desc', 'accepted_at']
-
end
-
-
1
def index_scope
-
5
default_scope.merge({ current_user_is_admin: model.admins.exists?(current_user.id), include_inviter: true })
-
end
-
-
1
def model
-
5
load_and_authorize(:group, :see_private_content, optional: true) ||
-
load_and_authorize(:discussion, optional: true) ||
-
load_and_authorize(:poll, optional: true) ||
-
NullGroup.new
-
end
-
-
1
def accessible_records
-
6
MembershipQuery.visible_to(user: current_user)
-
end
-
end
-
class API::V1::NotificationsController < API::V1::RestfulController
-
def index
-
self.collection = accessible_records.limit(50).select do |notification|
-
current_user.can? :show, notification.event.eventable
-
end
-
respond_with_collection
-
end
-
-
def viewed
-
service.viewed(user: current_user)
-
render json: { success: :ok }
-
end
-
-
def accessible_records
-
current_user.notifications.includes(:actor, event: :eventable).order(id: :desc)
-
end
-
end
-
1
class API::V1::OutcomesController < API::V1::RestfulController
-
1
def create_action
-
9
@event = service.create(**{resource_symbol => resource, actor: current_user, params: resource_params})
-
end
-
-
1
def exclude_types
-
16
%w[discussion event]
-
end
-
end
-
class API::V1::PollTemplatesController < API::V1::RestfulController
-
def index
-
group = current_user.groups.find_by(id: params[:group_id]) || NullGroup.new
-
-
if params[:key_or_id].present? && (params[:key_or_id].to_i.to_s == params[:key_or_id].to_s)
-
@poll_template = PollTemplate.find_by(group_id: current_user.group_ids, id: params[:key_or_id])
-
respond_with_resource
-
else
-
self.collection = PollTemplateService.group_templates(group: group)
-
respond_with_collection
-
end
-
end
-
-
def show
-
@poll_template = PollTemplate.find_by(group_id: current_user.group_ids, id: params[:id])
-
respond_with_resource
-
end
-
-
def update
-
if params[:id].to_i.to_s != params[:id].to_s
-
self.resource = PollTemplate.find_by(group_id: params[:poll_template][:group_id], key: params[:id])
-
else
-
load_resource
-
end
-
-
update_action
-
update_response
-
end
-
-
def positions
-
group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
params[:ids].each_with_index do |val, index|
-
if val.is_a? Integer
-
PollTemplate.where(id: val, group_id: group.id).update_all(position: index)
-
else
-
group.poll_template_positions[val] = index
-
end
-
end
-
-
group.save!
-
index
-
end
-
-
def settings
-
group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
if params.has_key?(:categorize_poll_templates)
-
group.categorize_poll_templates = params[:categorize_poll_templates]
-
group.save!
-
MessageChannelService.publish_models([group], group_id: group.id)
-
success_response
-
else
-
error_response(404)
-
end
-
end
-
-
def discard
-
@group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
@poll_template = @group.poll_templates.kept.find_by!(id: params[:id])
-
@poll_template.discard!
-
index
-
end
-
-
def undiscard
-
@group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
@poll_template = @group.poll_templates.discarded.find_by!(id: params[:id])
-
@poll_template.undiscard!
-
index
-
end
-
-
def destroy
-
@poll_template = PollTemplate.find(params[:id])
-
current_user.adminable_groups.find(@poll_template.group_id)
-
@poll_template.destroy!
-
destroy_response
-
end
-
-
def hide
-
@group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
-
if PollTemplateService.group_templates(group: @group).any? {|pt| pt.key == params[:key]}
-
@group = current_user.adminable_groups.find_by(id: params[:group_id])
-
@group.hidden_poll_templates ||= []
-
@group.hidden_poll_templates.push params[:key].parameterize
-
@group.hidden_poll_templates.uniq!
-
@group.save!
-
index
-
else
-
response_with_error(404)
-
end
-
end
-
-
def unhide
-
@group = current_user.adminable_groups.find_by!(id: params[:group_id])
-
-
if PollTemplateService.group_templates(group: @group).any? {|pt| pt.key == params[:key]}
-
@group = current_user.adminable_groups.find_by(id: params[:group_id])
-
@group.hidden_poll_templates -= [params[:key].parameterize]
-
@group.save!
-
self.resource = @group
-
index
-
else
-
response_with_error(404)
-
end
-
end
-
end
-
1
class API::V1::PollsController < API::V1::RestfulController
-
1
def show
-
2
self.resource = load_and_authorize(:poll)
-
1
accept_pending_membership
-
1
respond_with_resource
-
end
-
-
1
def remind
-
event = service.remind(poll: load_and_authorize(:poll), actor: current_user, params: resource_params)
-
render json: {count: event.recipient_user_ids.count}
-
end
-
-
1
def index
-
8
instantiate_collection do |collection|
-
8
PollQuery.filter(chain: collection, params: params).order(created_at: :desc)
-
end
-
8
respond_with_collection
-
end
-
-
1
def close
-
4
@event = service.close(poll: load_resource, actor: current_user)
-
1
respond_with_resource
-
end
-
-
1
def reopen
-
3
@event = service.reopen(poll: load_resource, params: resource_params, actor: current_user)
-
1
respond_with_resource
-
end
-
-
1
def discard
-
2
load_resource
-
2
@event = service.discard(poll: resource, actor: current_user)
-
1
respond_with_resource
-
end
-
-
1
def add_to_thread
-
1
@event = service.add_to_thread(poll: load_resource, params: params, actor: current_user)
-
1
respond_with_resource
-
end
-
-
1
def voters
-
load_and_authorize(:poll)
-
if !@poll.anonymous
-
self.collection = User.where(id: @poll.voter_ids)
-
else
-
self.collection = User.none
-
end
-
cache = RecordCache.for_collection(collection, current_user.id, exclude_types)
-
respond_with_collection serializer: AuthorSerializer, root: :users, scope: {cache: cache, exclude_types: exclude_types}
-
end
-
-
1
private
-
1
def create_action
-
9
@event = service.create(**{resource_symbol => resource, actor: current_user, params: resource_params})
-
end
-
-
1
def accessible_records
-
8
PollQuery.visible_to(user: current_user, show_public: false)
-
end
-
end
-
1
class API::V1::ProfileController < API::V1::RestfulController
-
1
before_action :require_current_user, only: [:index, :contactable]
-
-
1
def index
-
ids = UserQuery.invitable_user_ids(model: nil, actor: current_user, user_ids: params[:xids].split('x').map(&:to_i).compact)
-
self.collection = User.where(id: ids)
-
cache = RecordCache.for_collection(collection, current_user.id, exclude_types)
-
respond_with_collection serializer: AuthorSerializer, root: :users, scope: {cache: cache, exclude_types: exclude_types}
-
end
-
-
1
def show
-
3
load_and_authorize :user
-
2
respond_with_resource serializer: UserSerializer
-
end
-
-
1
def groups
-
self.collection = GroupQuery.visible_to(user: current_user)
-
-
cache = RecordCache.for_collection(collection, current_user.id, exclude_types)
-
-
respond_with_collection serializer: GroupSerializer, root: :groups, scope: {cache: cache, exclude_types: exclude_types}
-
end
-
-
1
def time_zones
-
time_zones = User.where('time_zone is not null').joins(:memberships).
-
where('memberships.group_id': current_user.group_ids).
-
group(:time_zone).count.sort_by {|k,v| -v }
-
render json: time_zones, root: false
-
end
-
-
1
def all_time_zones
-
zones = ActiveSupport::TimeZone.all.map do |tz|
-
{
-
title: I18n.t("timezones.#{tz.name}", default: tz.name, locale: params[:selected_locale]),
-
value: tz.tzinfo.name
-
}
-
end
-
render json: zones, root: false
-
end
-
-
1
def mentionable_users
-
4
instantiate_collection do |collection|
-
4
collection.distinct.mention_search(current_user, model, String(params[:q]).strip.delete("\u0000"))
-
end
-
4
respond_with_collection serializer: AuthorSerializer, root: :users
-
end
-
-
1
def me
-
2
raise CanCan::AccessDenied.new unless current_user.is_logged_in?
-
1
self.resource = current_user
-
1
respond_with_resource serializer: UserSerializer
-
end
-
-
1
def email_api_key
-
render json: {email_api_key: current_user.email_api_key}
-
end
-
-
1
def reset_email_api_key
-
current_user.update_attribute(:email_api_key, User.generate_unique_secure_token.slice(0,10))
-
render json: {email_api_key: current_user.email_api_key}
-
end
-
-
1
def remind
-
service.remind(user: load_resource, actor: current_user, model: load_and_authorize(:poll))
-
respond_with_resource
-
end
-
-
1
def update_profile
-
3
service.update(**current_user_params)
-
2
respond_with_resource
-
end
-
-
1
def set_volume
-
2
service.set_volume(user: current_user, actor: current_user, params: params.slice(:volume, :apply_to_all))
-
2
respond_with_resource
-
end
-
-
1
def upload_avatar
-
1
service.update user: current_user, actor: current_user, params: { uploaded_avatar: params[:file], avatar_kind: :uploaded }
-
1
respond_with_resource
-
end
-
-
1
def avatar_uploaded
-
render json: {avatar_uploaded: current_user.uploaded_avatar_url}
-
end
-
-
1
def deactivate
-
service.deactivate(user: current_user, actor: current_user)
-
respond_with_resource
-
end
-
-
1
def destroy
-
1
service.redact(user: current_user, actor: current_user)
-
1
respond_with_resource
-
end
-
-
1
def save_experience
-
3
raise ActionController::ParameterMissing.new(:experience) unless params.has_key?(:experience)
-
2
service.save_experience(user: current_user, actor: current_user, params: params)
-
1
respond_with_resource
-
end
-
-
1
def email_status
-
respond_with_resource(serializer: Pending::UserSerializer, scope: {})
-
end
-
-
1
def email_exists
-
render json: {email: params[:email], exists: User.where(email: params[:email]).any?}
-
end
-
-
1
def send_merge_verification_email
-
MergeUsersService.send_merge_verification_email(actor: current_user, target_email: params[:target_email])
-
success_response
-
end
-
-
1
def contactable
-
5
current_user.ability.authorize!(:contact, User.find(params[:user_id]))
-
4
success_response
-
end
-
-
1
private
-
1
def current_user
-
10
restricted_user || super
-
end
-
-
1
def model
-
4
load_and_authorize(:group, optional: true) ||
-
load_and_authorize(:discussion, optional: true) ||
-
load_and_authorize(:poll, optional: true) ||
-
load_and_authorize(:comment, optional: true) ||
-
load_and_authorize(:stance, optional: true) ||
-
load_and_authorize(:outcome, optional: true)
-
end
-
-
1
def accessible_records
-
4
resource_class
-
end
-
-
1
def resource
-
30
@user || current_user.presence || user_by_email
-
end
-
-
1
def user_by_email
-
resource_class.active.find_by(email: params[:email]) || LoggedOutUser.new(email: params[:email])
-
end
-
-
1
def deactivated_user
-
resource_class.deactivated.find_by(email: params[:user][:email])
-
end
-
-
1
def current_user_params
-
3
{ user: current_user, actor: current_user, params: permitted_params.user }
-
end
-
-
1
def resource_class
-
8
User
-
end
-
-
1
def serializer_class
-
7
if current_user.restricted
-
Restricted::UserSerializer
-
else
-
7
CurrentUserSerializer
-
end
-
end
-
-
1
def serializer_root
-
10
:users
-
end
-
-
1
def service
-
9
UserService
-
end
-
end
-
1
class API::V1::ReactionsController < API::V1::RestfulController
-
1
alias :create :update
-
-
1
def index
-
2
%w[comment_ids discussion_ids outcome_ids poll_ids stance_ids].each do |key|
-
10
next unless params.has_key? key
-
6
params[key] = params[key].split('x').map(&:to_i)
-
end
-
2
ReactionQuery.authorize!(user: current_user, params: params)
-
1
self.collection = ReactionQuery.unsafe_where(params)
-
1
respond_with_collection
-
end
-
-
1
private
-
-
1
def accessible_records
-
current_user.ability.authorize!(:show, reactable).reactions
-
end
-
-
1
def load_resource
-
2
self.resource = case action_name
-
2
when 'create', 'update' then resource_class.find_or_initialize_by(user: current_user, reactable: reactable)
-
else super
-
end
-
end
-
-
1
def reactable
-
2
@reactable ||= reactable_params[:reactable_type].classify.constantize.find(reactable_params[:reactable_id])
-
end
-
-
1
def reactable_params
-
4
case action_name
-
4
when 'create', 'update' then resource_params
-
when 'index' then params
-
end
-
end
-
end
-
1
class API::V1::ReceivedEmailsController < API::V1::RestfulController
-
1
def index
-
3
raise CanCan::AccessDenied unless current_user.adminable_group_ids.include?(params[:group_id].to_i)
-
1
instantiate_collection
-
1
respond_with_collection
-
end
-
-
1
def aliases
-
2
raise CanCan::AccessDenied unless current_user.adminable_group_ids.include?(params[:group_id].to_i)
-
1
aliases = MemberEmailAlias.where(group_id: params[:group_id])
-
1
render json: aliases, scope: default_scope, each_serializer: MemberEmailAliasSerializer, root: :aliases, meta: meta.merge({root: :aliases, total: collection_count})
-
end
-
-
1
def destroy_alias
-
member_email_alias = MemberEmailAlias.where(group_id: current_user.adminable_group_ids).find(params[:id])
-
member_email_alias.destroy
-
ReceivedEmailService.route_all
-
success_response
-
end
-
-
1
def allow
-
3
@received_email = ReceivedEmail.unreleased.where(group_id: current_user.adminable_group_ids).find(params[:id])
-
2
if @received_email.group.is_trial_or_demo?
-
1
respond_with_error(403, "trial groups cannot add aliases")
-
else
-
1
user = @received_email.group.members.find(params[:user_id])
-
1
MemberEmailAlias.create(
-
email: @received_email.sender_email,
-
user: user,
-
group_id: @received_email.group_id,
-
require_dkim: @received_email.dkim_valid,
-
require_spf: @received_email.spf_valid,
-
author_id: current_user.id
-
)
-
1
ReceivedEmailService.route(@received_email)
-
1
respond_with_resource
-
end
-
end
-
-
1
def block
-
2
@received_email = ReceivedEmail.unreleased.where(group_id: current_user.adminable_group_ids).find(params[:id])
-
1
MemberEmailAlias.create!(
-
email: @received_email.sender_email,
-
user_id: nil,
-
group_id: @received_email.group_id,
-
author_id: current_user.id
-
)
-
1
@received_email.update(group_id: nil)
-
1
respond_with_resource
-
end
-
-
1
private
-
-
1
def accessible_records
-
1
ReceivedEmail.where(group_id: params[:group_id], released: false)
-
end
-
end
-
1
class API::V1::RegistrationsController < Devise::RegistrationsController
-
1
include LocalesHelper
-
1
before_action :configure_permitted_parameters
-
1
before_action :permission_check, only: :create
-
-
1
def create
-
10
@email_can_be_verified = email_can_be_verified?
-
10
self.resource = UserService.create(params: sign_up_params)
-
9
if !resource.errors.any?
-
7
save_detected_locale(resource)
-
7
if @email_can_be_verified
-
2
sign_in resource
-
2
flash[:notice] = t(:'devise.sessions.signed_in')
-
2
render json: Boot::User.new(resource).payload.merge({ success: :ok, signed_in: true })
-
else
-
5
LoginTokenService.create(actor: resource, uri: URI::parse(request.referrer.to_s))
-
5
render json: { success: :ok, signed_in: false }
-
end
-
7
EventBus.broadcast('registration_create', resource)
-
else
-
2
render json: { errors: resource.errors }, status: 422
-
end
-
rescue UserService::EmailTakenError => e
-
1
render json: {errors: {email: [I18n.t('auth_form.email_taken')]}}, status: 422
-
end
-
-
1
private
-
1
def email_can_be_verified?
-
10
(pending_membership&.user ||
-
pending_login_token&.user ||
-
pending_discussion_reader&.user ||
-
pending_stance&.user ||
-
pending_identity)&.email == sign_up_params[:email]
-
end
-
-
1
def pending_user
-
2
user = (pending_membership || pending_login_token || pending_identity)&.user
-
2
user if user && !user.email_verified?
-
end
-
-
1
def permission_check
-
10
if !(AppConfig.app_features[:create_user] || pending_invitation || pending_group)
-
render json: { errors: {email: [I18n.t('auth_form.invitation_required')], name: [I18n.t('auth_form.invitation_required')]}}, status: 422
-
end
-
end
-
-
1
def configure_permitted_parameters
-
10
devise_parameter_sanitizer.permit(:sign_up) do |u|
-
20
u.permit(:name, :email, :recaptcha, :legal_accepted, :email_newsletter)
-
end
-
end
-
end
-
class API::V1::ReportsController < API::V1::RestfulController
-
def index
-
start_at = Date.parse(params.fetch(:start_month, 12.months.ago.to_date.iso8601[0..-4]) + "-01")
-
end_at = Date.parse(params.fetch(:end_month, Date.today.iso8601[0..-4]) + "-01") + 1.month
-
interval = params.fetch(:interval, 'month')
-
user_group_ids = current_user.group_ids
-
group_ids = params.fetch(:group_ids).split(',').map(&:to_i)
-
all_group_ids = Group.where("id IN (:group_ids) OR parent_id IN (:group_ids)", group_ids: Group.where(id: group_ids).pluck(:id, :parent_id).flatten.uniq).pluck(:id).uniq
-
all_groups = Group.where(id: all_group_ids).order("parent_id NULLS FIRST, name asc").pluck(:id, :name).map {|pair| {id: pair[0], name: pair[1] } }
-
-
if current_user.is_admin?
-
all_groups.unshift({id: 0, name: 'Direct threads'})
-
else
-
group_ids = group_ids & current_user.group_ids
-
all_group_ids = all_group_ids & current_user.group_ids
-
end
-
-
@report = ReportService.new(interval: interval, group_ids: group_ids, start_at: start_at, end_at: end_at)
-
render json: {
-
all_groups: all_groups,
-
intervals: @report.intervals,
-
-
comments_per_interval: @report.comments_per_interval,
-
discussions_per_interval: @report.discussions_per_interval,
-
polls_per_interval: @report.polls_per_interval,
-
stances_per_interval: @report.stances_per_interval,
-
outcomes_per_interval: @report.outcomes_per_interval,
-
-
discussions_count: @report.discussions_count,
-
discussions_with_polls_count: @report.discussions_with_polls_count,
-
polls_count: @report.polls_count,
-
polls_with_outcomes_count: @report.polls_with_outcomes_count,
-
-
tag_names: @report.tag_names,
-
discussion_tag_counts: @report.discussion_tag_counts,
-
poll_tag_counts: @report.poll_tag_counts,
-
tag_counts: @report.tag_counts,
-
users: @report.users.map {|u| {id: u.id, name: u.name, country: u.country} },
-
discussions_per_user: @report.discussions_per_user,
-
comments_per_user: @report.comments_per_user,
-
polls_per_user: @report.polls_per_user,
-
outcomes_per_user: @report.outcomes_per_user,
-
stances_per_user: @report.stances_per_user,
-
reactions_per_user: @report.reactions_per_user,
-
countries: @report.countries,
-
discussions_per_country: @report.discussions_per_country,
-
comments_per_country: @report.comments_per_country,
-
polls_per_country: @report.polls_per_country,
-
outcomes_per_country: @report.outcomes_per_country,
-
stances_per_country: @report.stances_per_country,
-
reactions_per_country: @report.reactions_per_country,
-
users_per_country: @report.users_per_country,
-
total_users: @report.users_per_country.values.sum.to_f,
-
}
-
end
-
end
-
1
class API::V1::RestfulController < API::V1::SnorlaxBase
-
1
include ActiveStorage::SetCurrent
-
1
include ::LocalesHelper
-
1
include ::ProtectedFromForgery
-
1
include ::LoadAndAuthorize
-
1
include ::CurrentUserHelper
-
1
include ::SentryHelper
-
1
include ::PendingActionsHelper
-
-
1
before_action :handle_pending_actions
-
1
around_action :use_preferred_locale # LocalesHelper
-
1
before_action :set_paper_trail_whodunnit # gem 'paper_trail'
-
1
before_action :set_sentry_context # SentryHelper
-
1
before_action :deny_spam_users # CurrentUserHelper
-
-
1
private
-
1
def require_current_user
-
5
unless current_user && current_user.is_logged_in?
-
render(json: {error: 'you gotta be signed in'}, root: false, status: 401)
-
end
-
end
-
end
-
1
class API::V1::SearchController < API::V1::RestfulController
-
1
def index
-
4
if group_or_org_id.to_i == 0
-
2
rel = PgSearch.multisearch(params[:query]).where("group_id is null and discussion_id IN (:discussion_ids)", discussion_ids: current_user.guest_discussion_ids)
-
end
-
-
4
if group_or_org_id.to_i > 0
-
2
rel = PgSearch.multisearch(params[:query]).where("group_id IN (:group_ids)", group_ids: group_ids)
-
end
-
-
4
if group_or_org_id.blank?
-
1
rel = PgSearch.multisearch(params[:query]).where("group_id IN (:group_ids) OR discussion_id in (:discussion_ids)", group_ids: group_ids, discussion_ids: current_user.guest_discussion_ids)
-
end
-
-
4
if params[:tag]
-
discussion_ids = Discussion.where(group_id: group_ids).where("tags @> ARRAY[?]::varchar[]", Array(params[:tag])).pluck(:id)
-
poll_ids = Poll.where(group_id: group_ids).where("tags @> ARRAY[?]::varchar[]", Array(params[:tag])).pluck(:id)
-
rel = rel.where("discussion_id in (:discussion_ids) or poll_id in (:poll_ids)", discussion_ids: discussion_ids, poll_ids: poll_ids)
-
end
-
-
4
if %w[Discussion Comment Poll Stance Outcome].include?(params[:type])
-
rel = rel.where(searchable_type: params[:type])
-
end
-
-
4
if params[:order] == 'authored_at_desc'
-
rel = rel.reorder('authored_at desc')
-
end
-
-
4
if params[:order] == 'authored_at_asc'
-
rel = rel.reorder('authored_at asc')
-
end
-
-
4
results = rel.limit(20).with_pg_search_highlight.all
-
# results = results.order().offset().limit()
-
-
4
groups = access_by_id(Group.where(id: results.map(&:group_id)))
-
4
discussions = access_by_id(Discussion.where(id: results.map(&:discussion_id)))
-
4
polls = access_by_id(Poll.where(id: results.map(&:poll_id)))
-
4
authors = access_by_id(User.where(id: results.map(&:author_id)))
-
-
4
poll_events = access_by_id(
-
Event.where("discussion_id is not null").where(eventable_type: "Poll", eventable_id: results.map(&:poll_id)),
-
:eventable_id
-
)
-
-
4
stance_events = access_by_id(
-
30
Event.where("discussion_id is not null").where(eventable_type: "Stance", eventable_id: results.filter {|r| r.searchable_type == 'Stance'}.map(&:searchable_id)),
-
:eventable_id
-
)
-
-
4
self.collection = results.map do |res|
-
30
poll = polls[res.poll_id]
-
30
discussion = discussions[res.discussion_id]
-
30
group = groups[res.group_id]
-
30
author = authors[res.author_id]
-
30
sequence_id = ((res.searchable_type == "Stance" && stance_events[res.searchable_id]) || poll_events[res.poll_id] || nil)&.sequence_id
-
30
SearchResult.new(
-
id: res.id,
-
searchable_type: res.searchable_type,
-
searchable_id: res.searchable_id,
-
poll_title: poll&.title,
-
discussion_title: discussion&.title,
-
discussion_key: discussion&.key,
-
highlight: res.pg_search_highlight,
-
poll_id: res.poll_id,
-
poll_key: poll&.key,
-
sequence_id: sequence_id,
-
group_handle: group&.handle,
-
group_key: group&.key,
-
group_id: group&.id,
-
group_name: group&.full_name,
-
author_name: author&.name,
-
author_id: res.author_id,
-
authored_at: res.authored_at,
-
30
tags: (Array(poll&.tags) + Array(discussion&.tags)).uniq
-
)
-
end
-
-
4
respond_with_collection
-
end
-
-
-
1
private
-
1
def access_by_id(collection, id_col = 'id')
-
24
h = {}
-
24
collection.each do |row|
-
58
h[row.send(id_col)] = row
-
end
-
24
h
-
end
-
-
1
def exclude_types
-
8
'group membership discussion outcome event'.split(' ')
-
end
-
-
1
def group_ids
-
3
if params[:group_id].present?
-
2
current_user.browseable_group_ids & Array(params[:group_id].to_i)
-
1
elsif params[:org_id] == '0'
-
[]
-
1
elsif params[:org_id].present?
-
current_user.browseable_group_ids & Group.find(params[:org_id]).id_and_subgroup_ids
-
else
-
1
current_user.browseable_group_ids
-
end
-
end
-
-
1
def group_or_org_id
-
12
params[:group_id] || params[:org_id]
-
end
-
-
1
def serializer_root
-
4
:search_results
-
end
-
-
1
def serializer_class
-
4
SearchResultSerializer
-
end
-
end
-
1
class API::V1::SessionsController < Devise::SessionsController
-
1
include PrettyUrlHelper
-
1
before_action :configure_permitted_parameters
-
-
1
def create
-
9
if user = attempt_login
-
4
sign_in(user)
-
4
flash[:notice] = t(:'devise.sessions.signed_in')
-
4
user.update(name: resource_params[:name]) if resource_params[:name]
-
4
render json: Boot::User.new(user).payload
-
4
EventBus.broadcast('session_create', user)
-
else
-
5
render json: { errors: failure_message }, status: 401
-
end
-
9
session.delete(:pending_login_token)
-
end
-
-
1
def destroy
-
sign_out resource_name
-
-
# temp fix because we've changed the session domain
-
if ENV['CANONICAL_HOST'] == 'www.loomio.org'
-
cookies.delete :_loomio, domain: '.loomio.org'
-
cookies.delete :remember_user_token, domain: '.loomio.org'
-
cookies.delete :_loomio
-
cookies.delete :remember_user_token
-
end
-
-
flash[:notice] = t(:'devise.sessions.signed_out')
-
render json: { success: :ok }
-
end
-
-
1
private
-
-
1
def failure_message
-
5
if resource_params[:password] && User.where(email: resource_params[:email]).where.not(locked_at: nil).exists?
-
{ password: [:'auth_form.account_locked'] }
-
else
-
5
{ password: [:'auth_form.invalid_password'] }
-
end
-
end
-
-
1
def attempt_login
-
9
if pending_login_token&.useable?
-
3
pending_login_token.user
-
6
elsif resource_params[:code]
-
login_token_user
-
else
-
6
warden.authenticate(scope: resource_name)
-
end
-
end
-
-
1
def login_token_user
-
token = LoginToken.unused.find_by(code: resource_params.require(:code))
-
token.user if token&.user&.email == resource_params.require(:email)
-
end
-
-
1
def configure_permitted_parameters
-
9
devise_parameter_sanitizer.permit(:sign_in) do |u|
-
u.permit(:code, :email, :password, :remember_me)
-
end
-
end
-
end
-
1
class API::V1::SnorlaxBase < ActionController::Base
-
105
rescue_from(CanCan::AccessDenied) { |e| respond_with_standard_error e, 403 }
-
2
rescue_from(Subscription::MaxMembersExceeded) { |e| respond_with_standard_error e, 403 }
-
6
rescue_from(ActionController::UnpermittedParameters) { |e| respond_with_standard_error e, 400 }
-
3
rescue_from(ActionController::ParameterMissing) { |e| respond_with_standard_error e, 400 }
-
9
rescue_from(ActiveRecord::RecordNotFound) { |e| respond_with_standard_error e, 404 }
-
4
rescue_from(ActiveRecord::RecordInvalid) { |e| respond_with_errors }
-
1
attr_accessor :collection_count
-
-
1
def show
-
respond_with_resource
-
end
-
-
1
def index
-
8
instantiate_collection
-
7
respond_with_collection
-
end
-
-
1
def create
-
41
instantiate_resource
-
41
create_action
-
25
create_response
-
end
-
-
1
def update
-
38
load_resource
-
38
update_action
-
25
update_response
-
end
-
-
1
def destroy
-
3
load_resource
-
3
destroy_action
-
1
destroy_response
-
end
-
-
1
private
-
-
1
def load_resource
-
if resource_class.respond_to?(:friendly)
-
self.resource = resource_class.friendly.find(params[:id])
-
else
-
self.resource = resource_class.find(params[:id])
-
end
-
end
-
-
1
def create_action
-
23
@event = service.create(**{resource_symbol => resource, actor: current_user})
-
end
-
-
1
def update_action
-
39
@event = service.update(**{resource_symbol => resource, params: resource_params, actor: current_user})
-
end
-
-
1
def destroy_action
-
3
service.destroy(**{resource_symbol => resource, actor: current_user})
-
end
-
-
1
def permitted_params
-
155
@permitted_params ||= PermittedParams.new(params)
-
end
-
-
1
def service
-
147
"#{resource_name}_service".camelize.constantize
-
end
-
-
1
def public_records
-
resource_class.visible_to_public.order(created_at: :desc)
-
end
-
-
1
def respond_with_resource(scope: default_scope, serializer: serializer_class, root: serializer_root)
-
127
if resource.errors.empty?
-
120
respond_with_collection scope: scope, serializer: serializer, root: root
-
else
-
7
respond_with_errors
-
end
-
end
-
-
1
def respond_ok
-
2
render json: {}, status: 200
-
end
-
-
1
def respond_with_collection(scope: default_scope, serializer: serializer_class, root: serializer_root)
-
223
render json: records_to_serialize, scope: scope, each_serializer: serializer, root: root, meta: meta.merge({root: root, total: collection_count})
-
end
-
-
1
def meta
-
224
@meta || {}
-
end
-
-
1
def add_meta(key, value)
-
3
@meta ||= {}
-
3
@meta[key] = value
-
end
-
-
# prefer this
-
1
def records_to_serialize
-
780
if @event.is_a?(Event)
-
205
Array(@event)
-
else
-
575
collection || Array(resource)
-
end
-
end
-
-
1
def serializer_class
-
160
record = records_to_serialize.first
-
160
if record.nil?
-
3
EventSerializer
-
157
elsif record.is_a? Event
-
58
EventSerializer
-
else
-
99
"#{record.class}Serializer".constantize
-
end
-
end
-
-
1
def serializer_root
-
167
record = records_to_serialize.first
-
167
if record.nil?
-
3
controller_name
-
164
elsif record.is_a? Event
-
58
'events'
-
else
-
106
record.class.to_s.underscore.pluralize
-
end
-
end
-
-
1
def default_scope
-
{
-
230
cache: RecordCache.for_collection(records_to_serialize, current_user.id, exclude_types),
-
current_user_id: current_user.id,
-
exclude_types: exclude_types
-
}
-
end
-
-
1
def exclude_types
-
407
params[:exclude_types].to_s.split(' ')
-
end
-
-
# phase this out
-
1
def events_to_serialize
-
return [] unless @event.is_a?(Event)
-
Array(@event)
-
end
-
-
# phase this out
-
1
def resources_to_serialize
-
1
collection || Array(resource)
-
end
-
-
-
1
def collection
-
802
instance_variable_get :"@#{resource_name.pluralize}"
-
end
-
-
1
def resource
-
498
instance_variable_get :"@#{resource_name}"
-
end
-
-
1
def resource=(value)
-
167
instance_variable_set :"@#{resource_name}", value
-
end
-
-
1
def collection=(value)
-
278
instance_variable_set :"@#{resource_name.pluralize}", value
-
end
-
-
1
def instantiate_resource
-
67
self.resource = resource_class.new(self.class.filter_params(resource_class, resource_params))
-
end
-
-
1
def self.filter_params(resource_class, resource_params)
-
207
newbie = resource_class.new
-
207
out = {}.with_indifferent_access
-
207
resource_params.each_pair do |k, v|
-
560
out[k.to_sym] = v if newbie.respond_to?("#{k}=")
-
end
-
207
out
-
end
-
-
1
def instantiate_collection
-
47
self.collection = accessible_records
-
45
self.collection = yield collection if block_given?
-
45
self.collection = timeframe_collection collection
-
45
self.collection_count = collection.count
-
45
self.collection = page_collection collection
-
45
self.collection = order_collection collection
-
end
-
-
1
def timeframe_collection(collection)
-
45
if resource_class.try(:has_timeframe?) && (params[:since] || params[:until])
-
4
parse_date_parameters # I feel like Rails should do this for me..
-
4
collection.within(params[:since], params[:until], params[:timeframe_for])
-
else
-
41
collection
-
end
-
end
-
-
1
def parse_date_parameters
-
12
%w(since until).each { |field| params[field] = DateTime.parse(params[field].to_s) if params[field] }
-
end
-
-
1
def page_collection(collection)
-
45
collection.offset(params[:from].to_i).limit((params[:per] || default_page_size).to_i)
-
end
-
-
1
def order_collection(collection)
-
45
if valid_orders.include?(params[:order])
-
collection.order(params[:order])
-
else
-
45
collection
-
end
-
end
-
-
1
def accessible_records
-
if current_user.is_logged_in?
-
visible_records
-
else
-
public_records
-
end
-
end
-
-
1
def visible_records
-
raise NotImplementedError.new
-
end
-
-
1
def valid_orders
-
39
[]
-
end
-
-
1
def public_records
-
raise NotImplementedError.new
-
end
-
-
1
def default_page_size
-
44
50
-
end
-
-
1
def update_response
-
26
respond_with_resource
-
end
-
-
1
def create_response
-
24
respond_with_resource
-
end
-
-
1
def destroy_response
-
1
success_response
-
end
-
-
1
def success_response
-
5
render json: {success: 'success'}
-
end
-
-
1
def error_response(status = 500)
-
3
render json: {error: status}, root: false, status: status
-
end
-
-
1
def load_resource
-
85
self.resource = resource_class.find(params[:id])
-
end
-
-
1
def resource_params
-
161
permitted_params.send resource_name
-
end
-
-
1
def resource_symbol
-
98
resource_name.to_sym
-
end
-
-
1
def resource_name
-
2424
controller_name.singularize
-
end
-
-
1
def resource_class
-
263
resource_name.camelize.constantize
-
end
-
-
1
def respond_with_standard_error(error, status)
-
120
render json: {exception: error.class, error: error.to_s}, root: false, status: status
-
end
-
-
1
def respond_with_error(status, message = "error")
-
1
render json: {error: message}, root: false, status: status
-
end
-
-
1
def respond_with_errors(record = resource)
-
11
render json: {errors: record.errors.as_json}, root: false, status: 422
-
end
-
end
-
1
class API::V1::StancesController < API::V1::RestfulController
-
1
def create
-
12
super
-
rescue ActiveRecord::RecordNotUnique
-
1
self.resource = resource_class.find_by!(
-
poll_id: params[:stance][:poll_id],
-
participant_id: current_user.id)
-
1
update_action
-
1
update_response
-
end
-
-
1
def uncast
-
3
@stance = current_user.stances.latest.find(params[:id])
-
2
StanceService.uncast(stance: @stance, actor: current_user)
-
1
respond_with_recent_stances
-
end
-
-
1
def index
-
3
instantiate_collection do |collection|
-
2
if !@poll.anonymous && name = params[:name].presence
-
collection = collection.
-
joins('LEFT OUTER JOIN users on stances.participant_id = users.id').
-
where(latest: true, revoked_at: nil).
-
where("users.name ilike :first OR
-
users.name ilike :last OR
-
users.email ilike :first OR
-
users.username ilike :first",
-
first: "#{name}%", last: "% #{name}%")
-
end
-
-
2
if @poll.show_results?(voted: true)
-
2
if poll_option_id = params[:poll_option_id].presence
-
collection = collection.joins(:poll_options).where("poll_options.id" => poll_option_id)
-
end
-
end
-
-
2
collection.order('cast_at DESC NULLS LAST, created_at DESC')
-
end
-
2
respond_with_collection
-
end
-
-
1
def users
-
1
instantiate_collection do |collection|
-
1
if query = params[:query]
-
collection = collection.
-
joins('LEFT OUTER JOIN users on stances.participant_id = users.id').
-
where("users.name ilike :first OR
-
users.name ilike :last OR
-
users.email ilike :first OR
-
users.username ilike :first",
-
first: "#{query}%", last: "% #{query}%")
-
end
-
-
1
user_ids = collection.pluck(:participant_id)
-
1
self.add_meta :guest_ids, collection.where(guest: true).pluck(:participant_id) & user_ids
-
1
self.add_meta :member_admin_ids, @poll.group.admins.pluck(:user_id) & user_ids
-
1
self.add_meta :stance_admin_ids, collection.where(admin: true).pluck(:participant_id) & user_ids
-
1
User.where(id: collection.pluck(:participant_id))
-
end
-
1
respond_with_collection serializer: AuthorSerializer
-
end
-
-
1
def my_stances
-
self.collection = current_user.stances.latest.includes({poll: :discussion})
-
self.collection = collection.where('polls.discussion_id': @discussion.id) if load_and_authorize(:discussion, optional: true)
-
self.collection = collection.where('discussions.group_id': @group.id) if load_and_authorize(:group, optional: true)
-
respond_with_collection
-
end
-
-
1
def make_admin
-
3
@stance = Stance.latest.find_by(participant_id: params[:participant_id], poll_id: params[:poll_id])
-
3
current_user.ability.authorize! :make_admin, @stance
-
1
@stance.update(admin: true)
-
1
respond_with_resource
-
end
-
-
1
def remove_admin
-
1
@stance = Stance.latest.find_by(participant_id: params[:participant_id], poll_id: params[:poll_id])
-
1
current_user.ability.authorize! :remove_admin, @stance
-
1
@stance.update(admin: false)
-
1
@stance.poll.update_counts!
-
1
respond_with_resource
-
end
-
-
1
def revoke
-
2
@stance = Stance.latest.find_by(participant_id: params[:participant_id], poll_id: params[:poll_id])
-
2
current_user.ability.authorize! :remove, @stance
-
-
# revoke all stances, not just the latest one
-
1
Stance.where(revoked_at: nil, participant_id: params[:participant_id], poll_id: params[:poll_id]).
-
update_all(revoked_at: Time.zone.now, revoker_id: current_user.id)
-
-
1
@stance.reload
-
1
@stance.poll.update_counts!
-
-
1
@stances = @stance.poll.stances.where(participant_id: params[:participant_id])
-
1
live_update_outdated_stances(@stance.poll)
-
1
respond_with_collection
-
end
-
-
1
private
-
-
1
def live_update_outdated_stances(poll)
-
1
return if poll.discussion.nil?
-
# want to find stances with comments
-
stance_ids = poll.discussion.items.where(
-
eventable_type: 'Stance',
-
eventable_id: poll.stances.with_reason.where(latest: false).pluck(:id)
-
).where("child_count > 0").pluck('eventable_id')
-
stances = Stance.where(id: stance_ids).order('id desc').limit(50)
-
MessageChannelService.publish_models(stances, group_id: poll.group_id, user_id: current_user.id)
-
end
-
-
1
def respond_with_recent_stances
-
1
@event = nil
-
1
@stances = @stance.poll.stances.where(revoked_at: nil, participant_id: current_user.id).order('id desc').limit(10)
-
1
respond_with_collection
-
end
-
-
1
def current_user_is_admin?
-
16
stance = Stance.find_by(id: params[:id])
-
16
poll = Poll.find_by(id: params[:poll_id])
-
16
return false unless (stance || poll)
-
12
(stance || poll).poll.admins.exists?(current_user.id)
-
end
-
-
1
def exclude_types
-
32
%w[group discussion]
-
end
-
-
1
def default_scope
-
16
super.merge({include_email: current_user_is_admin?})
-
end
-
-
1
def accessible_records
-
4
load_and_authorize(:poll).stances.latest
-
end
-
end
-
1
class API::V1::TagsController < API::V1::RestfulController
-
1
def priority
-
load_and_authorize_group
-
Array(params[:ids]).each_with_index do |id, index|
-
Tag.where(id: id, group_id: @group.id).update_all(priority: index)
-
end
-
-
instantiate_collection
-
-
# rember to live update too
-
respond_with_collection
-
end
-
-
1
private
-
1
def respond_with_group
-
1
self.resource = resource.group.reload
-
1
respond_with_resource
-
end
-
-
1
def create_response
-
1
respond_with_group
-
end
-
-
1
def destroy_response
-
respond_with_group
-
end
-
-
1
def accessible_records
-
Tag.where(group_id: @group.id)
-
end
-
-
1
def load_and_authorize_group
-
@group = Group.find(params[:group_id])
-
current_user.ability.authorize!(:update, @group)
-
end
-
end
-
1
class API::V1::TasksController < API::V1::RestfulController
-
1
def index
-
# return tasks
-
-
1
self.collection = Task.joins('left outer join tasks_users on tasks_users.task_id = tasks.id')
-
.where("author_id = :user_id OR doer_id = :user_id OR tasks_users.user_id = :user_id", user_id: current_user.id)
-
-
1
respond_with_collection
-
end
-
-
1
def update_done
-
2
@task = Task.find_by(record: record, uid: params[:uid])
-
2
current_user.ability.authorize!(:update, @task)
-
-
2
TaskService.update_done(@task, current_user, params[:done] == 'true')
-
-
2
respond_with_resource
-
# we should also serialize the assocated record
-
end
-
-
1
def mark_as_done
-
2
@task = Task.find(params[:id])
-
2
current_user.ability.authorize!(:update, @task)
-
-
2
TaskService.update_done(@task, current_user, true)
-
-
2
respond_with_resource
-
# we should also serialize the assocated record
-
end
-
-
1
def mark_as_not_done
-
1
@task = Task.find(params[:id])
-
1
current_user.ability.authorize!(:update, @task)
-
-
1
TaskService.update_done(@task, current_user, false)
-
-
1
respond_with_resource
-
end
-
-
1
private
-
1
def record
-
2
load_and_authorize(:group, optional: true) ||
-
load_and_authorize(:discussion, optional: true) ||
-
load_and_authorize(:comment, optional: true) ||
-
load_and_authorize(:poll, optional: true) ||
-
load_and_authorize(:outcome, optional: true)
-
end
-
end
-
class API::V1::TranslationsController < API::V1::RestfulController
-
def inline
-
self.resource = service.create(model: load_and_authorize(params[:model]), to: params[:to])
-
respond_with_resource
-
end
-
end
-
1
class API::V1::TrialsController < API::V1::RestfulController
-
1
def create
-
1
email = params[:user_email].strip
-
-
1
user = User.verified.find_by(email: email)
-
1
if !user
-
1
user = User.where(email_verified: false, email: email).first_or_create
-
1
user.name = params[:user_name].strip
-
1
user.recaptcha = params[:recaptcha]
-
1
user.legal_accepted = true
-
1
user.email_newsletter = !!params[:newsletter]
-
# user.require_valid_signup = true
-
# user.require_recaptcha = true
-
1
user.save!
-
end
-
-
1
raise "you said I'd have a user by now" unless user && user.valid?
-
-
1
group = Group.new
-
1
group.assign_attributes_and_files(params.require(:group).permit(permitted_params.group_attributes))
-
1
group.group_privacy = "secret"
-
1
group.category = params[:group_category]
-
1
group.info['how_did_you_hear_about_loomio'] = params[:how_did_you_hear_about_loomio]
-
-
1
group.handle = GroupService.suggest_handle(name: group.name, parent_handle: nil)
-
1
GroupService.create(group: group, actor: user, skip_authorize: true)
-
-
1
raise "start trial failed" unless group.valid?
-
-
1
group_path = group.handle ? group_handle_path(group.handle) : group_path(group)
-
-
1
render json: {success: :ok, group_path: group_path}
-
end
-
end
-
1
class API::V1::VersionsController < API::V1::RestfulController
-
1
def show
-
2
self.resource = model.versions[params[:index].to_i]
-
1
respond_with_resource
-
end
-
-
1
private
-
-
1
def exclude_types
-
2
%w[discussion group user comment poll stance stance_choice]
-
end
-
-
1
def serializer_class
-
1
VersionSerializer
-
end
-
-
1
def serializer_root
-
1
'versions'
-
end
-
-
1
def model
-
2
load_and_authorize(:group, optional:true) ||
-
load_and_authorize(:discussion, optional:true) ||
-
load_and_authorize(:comment, optional:true) ||
-
load_and_authorize(:stance, optional:true) ||
-
load_and_authorize(:poll, optional:true) ||
-
load_and_authorize(:outcome, optional:false)
-
end
-
end
-
class ApplicationController < ActionController::Base
-
include LocalesHelper
-
include ProtectedFromForgery
-
include CurrentUserHelper
-
include SentryHelper
-
include PrettyUrlHelper
-
include LoadAndAuthorize
-
include EmailHelper
-
include ApplicationHelper
-
helper :email
-
helper :formatted_date
-
-
around_action :process_time_zone # LocalesHelper
-
around_action :use_preferred_locale # LocalesHelper
-
before_action :deny_spam_users # CurrentUserHelper
-
before_action :set_last_seen_at # CurrentUserHelper
-
before_action :handle_pending_actions # PendingActionsHelper
-
before_action :set_sentry_context
-
before_action :ensure_canonical_host
-
-
helper_method :current_user
-
helper_method :current_version
-
helper_method :bundle_asset_path
-
helper_method :supported_locales
-
helper_method :is_old_browser?
-
-
skip_before_action :verify_authenticity_token, only: :bug_tunnel
-
caches_page :sitemap
-
-
rescue_from(ActionController::UnknownFormat) do
-
respond_with_error message: :"errors.not_found", status: 404
-
end
-
-
rescue_from(ActionView::MissingTemplate) do |exception|
-
raise exception unless %w[txt text gif png].include?(params[:format])
-
end
-
-
rescue_from(ActiveRecord::RecordNotFound) do
-
respond_with_error message: :"errors.not_found", status: 404
-
end
-
-
rescue_from(Membership::InvitationAlreadyUsed) do |exception|
-
session.delete(:pending_membership_token)
-
if current_user.ability.can?(:show, exception.membership.group)
-
redirect_to polymorphic_path(exception.membership.group) || dashboard_path
-
else
-
respond_with_error message: :"invitation.invitation_already_used"
-
end
-
end
-
-
rescue_from(CanCan::AccessDenied) do |exception|
-
if current_user.is_logged_in?
-
flash[:error] = t("error.access_denied")
-
redirect_to dashboard_path
-
else
-
authenticate_user!
-
end
-
end
-
-
def response_format
-
params[:format] == 'json' ? :json : :html
-
end
-
-
def respond_with_error(message: nil, status: 400)
-
@title = t("errors.#{status}.body")
-
@body = t(message || "errors.#{status}.body")
-
render "application/error", layout: 'basic', status: status, formats: response_format
-
end
-
-
def index
-
boot_app
-
end
-
-
def sitemap
-
@entries = []
-
Group.published.where(is_visible_to_public: true).each do |g|
-
@entries << [url_for(g), g.updated_at.to_date.iso8601]
-
end
-
-
Discussion.visible_to_public.joins(:group).where('groups.archived_at is null').each do |d|
-
@entries << [url_for(d), d.last_activity_at.to_date.iso8601]
-
end
-
end
-
-
def show
-
resource = ModelLocator.new(resource_name, params).locate!
-
@recipient = current_user
-
if current_user.can? :show, resource
-
assign_resource
-
@pagination = pagination_params
-
respond_to do |format|
-
format.html
-
format.rss { render :"show.xml" }
-
format.xml
-
end
-
else
-
boot_app(status: 403)
-
end
-
end
-
-
def crowdfunding
-
render layout: 'basic'
-
end
-
-
def brand
-
render layout: 'basic'
-
end
-
-
def bug_tunnel
-
raise "no sentry dsn" unless ENV['SENTRY_PUBLIC_DSN']
-
-
uri = URI(ENV['SENTRY_PUBLIC_DSN'])
-
known_host = uri.host
-
known_project_id = uri.path.tr('/', '')
-
-
envelope = request.body.read
-
piece = envelope.split("\n").first
-
header = JSON.parse(piece)
-
dsn = URI.parse(header['dsn'])
-
project_id = dsn.path.tr('/', '')
-
-
raise "Invalid sentry hostname: #{dsn.hostname}" if dsn.hostname != known_host
-
raise "Invalid sentry project id: #{project_id}" if project_id != known_project_id
-
-
upstream_sentry_url = "https://#{known_host}/api/#{known_project_id}/envelope/"
-
Net::HTTP.post(URI(upstream_sentry_url), envelope)
-
-
head(:ok)
-
rescue => e
-
# handle exception in your preferred style,
-
# e.g. by logging or forwarding to server Sentry project
-
Rails.logger.error('error tunneling to sentry')
-
end
-
-
def ok
-
head :ok
-
end
-
-
protected
-
def pagination_params
-
default_limit = (params[:export]) ? 2000 : 10
-
{ limit: params.fetch(:limit, default_limit).to_i, offset: params.fetch(:offset, 0).to_i }
-
end
-
-
def prevent_caching
-
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' # HTTP 1.1.
-
response.headers['Pragma'] = 'no-cache' # HTTP 1.0.
-
response.headers['Expires'] = '0' # Proxies.
-
end
-
-
private
-
-
def ensure_canonical_host
-
if ENV['REDIRECT_TO_CANONICAL_HOST']
-
if request.host != ENV['CANONICAL_HOST']
-
u = URI(request.url)
-
u.host = ENV['CANONICAL_HOST']
-
redirect_to u.to_s, status: :moved_permanently
-
end
-
end
-
end
-
-
def boot_app(status: 200)
-
expires_now
-
prevent_caching
-
render file: Rails.root.join('public/blient/index.html'), layout: false, status: status
-
end
-
-
def redirect_to(url, opts = {})
-
return super unless url.is_a? String # GK: for now this override only covers cases where a string has been passed in, so it does not cover cases of a Hash or a Record being passed in
-
host = URI(url).host
-
if ENV['USE_VUE'] && Rails.env.development? && host == "localhost"
-
path = URI(url).path
-
query = URI(url).query ? "?#{URI(url).query}" : ""
-
super "http://localhost:8080#{path}#{query}"
-
else
-
super
-
end
-
end
-
-
def is_old_browser?
-
browser.ie? || (browser.safari? && browser.version.to_i < 12)
-
end
-
end
-
1
class AuthenticateByUnsubscribeTokenController < ApplicationController
-
1
before_action :authenticate_user_by_unsubscribe_token_or_fallback
-
-
1
private
-
-
1
def user
-
13
@restricted_user || current_user
-
end
-
-
1
def authenticate_user_by_unsubscribe_token_or_fallback
-
7
unless (params[:unsubscribe_token].present? and @restricted_user = User.find_by_unsubscribe_token(params[:unsubscribe_token]))
-
authenticate_user!
-
end
-
end
-
end
-
class Dev::BaseController < ApplicationController
-
before_action :ensure_not_production
-
-
def index
-
@routes = self.class.action_methods.select do |action|
-
/^(test_|setup_|view_)/.match action
-
end
-
render 'dev/main/index', layout: false
-
end
-
-
def import_test_data
-
GroupExportService.import('tmp/test.json')
-
sign_in User.first
-
redirect_to Group.order('memberships_count desc').first
-
end
-
-
def last_email(to: nil)
-
@email = if to.present?
-
ActionMailer::Base.deliveries.filter { |email| Array(email.to).include?(to.email) }
-
else
-
ActionMailer::Base.deliveries
-
end.last
-
render template: 'dev/main/last_email', layout: false
-
end
-
-
private
-
-
def ensure_not_production
-
raise "Development and testing only" if Rails.env.production?
-
end
-
end
-
class Dev::DiscussionsController < Dev::BaseController
-
include Dev::FakeDataHelper
-
-
def test_none_read
-
discussion = create_discussion_with_nested_comments
-
sign_in discussion.author
-
redirect_to discussion_url(discussion)
-
end
-
-
def test_some_read
-
discussion = create_discussion_with_nested_comments
-
EventService.repair_thread(discussion.id)
-
discussion.author.experienced!('betaFeatures')
-
sign_in discussion.author
-
read_ids = discussion.items.order(sequence_id: :asc).limit(5).pluck(:sequence_id)
-
DiscussionReader.for_model(discussion, discussion.author).viewed!(read_ids)
-
redirect_to discussion_url(discussion)
-
end
-
-
def test_most_read
-
discussion = create_discussion_with_nested_comments
-
sign_in discussion.author
-
read_ids = discussion.items.order(sequence_id: :asc).limit(5).pluck(:sequence_id)
-
DiscussionReader.for_model(discussion, discussion.author).viewed!(read_ids)
-
redirect_to discussion_url(discussion)
-
end
-
-
def test_all_read
-
discussion = create_discussion_with_nested_comments
-
sign_in discussion.author
-
read_ids = discussion.items.order(sequence_id: :asc).pluck(:sequence_id)
-
DiscussionReader.for_model(discussion, discussion.author).viewed!(read_ids)
-
redirect_to discussion_url(discussion)
-
end
-
-
def test_sampled_comments
-
discussion = create_discussion_with_sampled_comments
-
sign_in discussion.author
-
redirect_to discussion_url(discussion)
-
end
-
end
-
class Dev::NightwatchController < Dev::BaseController
-
include Dev::NintiesMoviesHelper
-
include PrettyUrlHelper
-
-
include Dev::Scenarios::Util
-
include Dev::Scenarios::Auth
-
include Dev::Scenarios::Dashboard
-
include Dev::Scenarios::Discussion
-
include Dev::Scenarios::EmailSettings
-
include Dev::Scenarios::Group
-
include Dev::Scenarios::Inbox
-
include Dev::Scenarios::JoinGroup
-
include Dev::Scenarios::MembershipRequest
-
include Dev::Scenarios::Membership
-
include Dev::Scenarios::Notification
-
include Dev::Scenarios::Profile
-
include Dev::Scenarios::Tags
-
-
before_action :redis_flushall, except: [
-
:last_email,
-
:use_last_login_token,
-
:index,
-
:accept_last_invitation,
-
]
-
before_action :cleanup_database, except: [
-
:last_email,
-
:use_last_login_token,
-
:index,
-
:accept_last_invitation,
-
]
-
-
-
def redis_flushall
-
CACHE_REDIS_POOL.with do |client|
-
client.flushall
-
end
-
end
-
end
-
class Dev::PollsController < Dev::NightwatchController
-
include Dev::ScenariosHelper
-
-
def test_poll_scenario
-
scenario =send(:"#{params[:scenario]}_scenario", {
-
poll_type: params[:poll_type],
-
anonymous: !!params[:anonymous],
-
hide_results: (params[:hide_results] || :off),
-
admin: !!params[:admin],
-
guest: !!params[:guest],
-
standalone: !!params[:standalone],
-
wip: !!params[:wip]
-
})
-
-
scenario[:group].add_admin! scenario[:observer]
-
-
sign_in(scenario[:observer]) if scenario[:observer].is_a?(User)
-
-
if params[:email]
-
@scenario = scenario
-
last_email to: scenario[:observer]
-
else
-
redirect_to poll_url(scenario[:poll], Hash(scenario[:params]))
-
end
-
end
-
-
def test_invite_to_poll
-
admin = saved fake_user
-
group = saved fake_group
-
group.add_admin! admin
-
-
if params[:guest]
-
user = saved fake_unverified_user
-
else
-
user = saved fake_user
-
group.add_member! user
-
end
-
-
discussion = fake_discussion(group: group)
-
-
DiscussionService.create(discussion: discussion, actor: admin)
-
-
# select poll type here
-
poll = fake_poll(group: group, discussion: discussion, author: admin)
-
PollService.create(poll: poll, actor: poll.author, params: {notify_recipients: true})
-
-
if params[:guest]
-
PollService.invite(poll: poll, params: {recipient_emails: [user.email], notify_recipients: true}, actor: poll.author)
-
end
-
-
last_email
-
end
-
-
def test_discussion
-
group = create_group_with_members
-
sign_in group.admins.first
-
discussion = saved fake_discussion(group: group, author: group.admins.first)
-
DiscussionService.create(discussion: discussion, actor: discussion.author)
-
redirect_to discussion_path(discussion)
-
end
-
-
def test_poll_in_discussion
-
group = create_group_with_members
-
sign_in group.admins.first
-
discussion = saved fake_discussion(group: group, author: group.admins.first)
-
DiscussionService.create(discussion: discussion, actor: discussion.author)
-
poll = saved fake_poll(discussion: discussion)
-
stance = saved fake_stance(poll: poll)
-
StanceService.create(stance: stance, actor: group.members.last)
-
redirect_to poll_url(poll)
-
end
-
-
def start_poll
-
sign_in saved fake_user
-
redirect_to new_poll_url
-
end
-
-
def test_activity_items
-
user = fake_user
-
group = saved fake_group
-
group.add_admin! user
-
discussion = saved fake_discussion(group: group)
-
DiscussionService.create(discussion: discussion, actor: discussion.author)
-
-
sign_in user
-
create_activity_items(discussion: discussion, actor: user)
-
redirect_to discussion_url(discussion)
-
end
-
-
private
-
-
def create_activity_items(discussion: , actor: )
-
# create poll
-
options = {poll: %w[apple turnip peach],
-
count: %w[yes no],
-
proposal: %w[agree disagree abstain block],
-
dot_vote: %w[birds bees trees]}
-
-
AppConfig.poll_types.keys.each do |poll_type|
-
poll = Poll.new(poll_type: poll_type,
-
title: poll_type,
-
details: 'fine print',
-
poll_option_names: options[poll_type.to_sym],
-
discussion: discussion)
-
PollService.create(poll: poll, actor: actor)
-
-
# edit the poll
-
PollService.update(poll: poll, params: {title: 'choose!'}, actor: actor)
-
-
# vote on the poll
-
stance = Stance.new(poll: poll,
-
choice: poll.poll_option_names.first,
-
reason: 'democracy is in my shoes')
-
StanceService.create(stance: stance, actor: actor)
-
-
# close the poll
-
PollService.close(poll: poll, actor: actor)
-
-
# set an outcome
-
outcome = Outcome.new(poll: poll, statement: 'We all voted')
-
OutcomeService.create(outcome: outcome, actor: actor)
-
-
# create poll
-
poll = Poll.new(poll_type: poll_type,
-
title: 'Which one?',
-
details: 'fine print',
-
poll_option_names: options[poll_type.to_sym],
-
discussion: discussion)
-
PollService.create(poll: poll, actor: actor)
-
poll.update_attribute(:closing_at, 1.day.ago)
-
-
# expire the poll
-
PollService.expire_lapsed_polls
-
end
-
end
-
end
-
module Dev::Scenarios::Auth
-
def setup_invitation_email_to_visitor
-
group = create_group
-
params = {recipient_emails: ['newuser@example.com'], recipient_message: 'Hey this is the app I told you about. please accept the inviitation!'}
-
-
GroupService.invite(group:group, params: params, actor: group.creator)
-
-
last_email
-
end
-
-
def setup_invite_user_with_alternative_email
-
group = create_group
-
group.update(group_privacy: 'secret')
-
user = User.create(email: 'existing-user@example.com',
-
name: 'existing user',
-
email_verified: true,
-
password: 'veryeasytoguess123')
-
-
GroupService.invite(
-
group:group,
-
params: {
-
recipient_message: "hi, please join our sweet group!",
-
recipient_emails: ['newuser@example.com']
-
},
-
actor: group.creator)
-
-
sign_in user if params[:signed_in]
-
-
last_email
-
end
-
-
def setup_invite_user_with_correct_email
-
group = create_group
-
group.update(group_privacy: 'secret')
-
user = User.create(email: 'existing-user@example.com',
-
name: 'existing user',
-
email_verified: true,
-
password: 'veryeasytoguess123')
-
-
params = {recipient_emails: ['existing-user@example.com'], recipient_message: "hi, please join our sweet group!"}
-
-
GroupService.invite(group:group, params: params, actor: group.creator)
-
-
sign_in user if params[:signed_in]
-
-
last_email
-
end
-
-
def setup_invitation_email_to_user_with_password
-
group = create_group
-
another_group = saved fake_group
-
user = saved fake_user(password: nil, name: 'fake user')
-
another_group.add_member! user
-
another_group.add_member! group.creator
-
user.reload
-
group.creator.reload
-
params = {recipient_user_ids: [user.id], recipient_message: "click accept,
-
please
-
thanks" }
-
-
GroupService.invite(group:group, params: params, actor: group.creator)
-
-
last_email
-
end
-
-
def setup_membership_request_email
-
group = saved fake_group(is_visible_to_public: true, membership_granted_upon: 'approval')
-
admin = saved fake_user
-
GroupService.create(group: group, actor: admin)
-
user = saved fake_user
-
membership_request = ::MembershipRequest.new(requestor: user, group: group, introduction: "Hey, I'm a shady person who just wants to post spam into your group!")
-
-
MembershipRequestService.create(
-
membership_request: membership_request,
-
actor: user
-
)
-
-
sign_in admin
-
last_email
-
end
-
-
def setup_deactivated_user
-
patrick.update(deactivated_at: 1.day.ago)
-
redirect_to dashboard_url
-
end
-
-
def setup_login_token
-
login_token = FactoryBot.create(:login_token, user: patrick)
-
redirect_to(login_token_url(login_token.token))
-
end
-
-
def setup_login_token_email
-
login_token = FactoryBot.create(:login_token, user: patrick)
-
UserMailer.login(patrick.id, login_token.id).deliver
-
redirect_to('/dev/last_email')
-
end
-
-
def setup_used_login_token
-
login_token = FactoryBot.create(:login_token, user: patrick, used: true)
-
redirect_to(login_token_url(login_token.token))
-
end
-
-
def setup_explore_as_visitor
-
patrick
-
recent_discussion
-
redirect_to explore_url
-
end
-
-
def view_closed_group_with_shareable_link
-
redirect_to join_url(create_group)
-
end
-
-
def view_open_discussion_as_visitor
-
@group = Group.create!(name: 'Open Dirty Dancing Shoes',
-
membership_granted_upon: 'request',
-
group_privacy: 'open')
-
@group.add_member! patrick
-
@group.add_admin! jennifer
-
@discussion = Discussion.new(title: 'I carried a watermelon', private: false, author: jennifer, group: @group)
-
DiscussionService.create(discussion: @discussion, actor: @discussion.author)
-
redirect_to discussion_url(@discussion)
-
end
-
-
def view_closed_group_as_non_member
-
sign_in patrick
-
@group = Group.create!(name: 'Closed Dirty Dancing Shoes',
-
group_privacy: 'closed',
-
discussion_privacy_options: 'public_or_private')
-
@group.add_admin! jennifer
-
@discussion = Discussion.new(title: "I carried a watermelon", private: false, author: jennifer, group: @group)
-
DiscussionService.create(discussion: @discussion, actor: @discussion.author)
-
redirect_to group_url(@group)
-
end
-
-
def view_secret_group_as_non_member
-
patrick.update(is_admin: false)
-
sign_in patrick
-
@group = Group.create!(name: 'Secret Dirty Dancing Shoes',
-
group_privacy: 'secret')
-
redirect_to group_url(@group)
-
end
-
-
def view_closed_group_as_visitor
-
@group = Group.create!(name: 'Closed Dirty Dancing Shoes',
-
membership_granted_upon: 'approval',
-
group_privacy: 'closed',
-
discussion_privacy_options: 'public_or_private')
-
@group.add_member! patrick
-
@group.add_admin! jennifer
-
@discussion = @group.discussions.create!(title: 'This thread is private', private: true, author: jennifer)
-
DiscussionService.create(discussion: @discussion, actor: @discussion.author)
-
@public_discussion = @group.discussions.create!(title: 'This thread is public', private: false, author: jennifer)
-
DiscussionService.create(discussion: @public_discussion, actor: @public_discussion.author)
-
redirect_to group_url(@group)
-
end
-
-
def view_secret_group_as_visitor
-
@group = Group.create!(name: 'Secret Dirty Dancing Shoes',
-
group_privacy: 'secret')
-
@group.add_admin! patrick
-
redirect_to group_url(@group)
-
end
-
end
-
module Dev::Scenarios::Dashboard
-
include Dev::DashboardHelper
-
-
def setup_dashboard
-
sign_in patrick
-
pinned_discussion
-
poll_discussion
-
recent_discussion
-
redirect_to dashboard_url
-
end
-
-
def setup_dashboard_with_one_thread
-
sign_in patrick
-
recent_discussion
-
redirect_to dashboard_url
-
end
-
-
def setup_dashboard_as_visitor
-
patrick; jennifer
-
recent_discussion
-
redirect_to dashboard_url
-
end
-
end
-
module Dev::Scenarios::Discussion
-
def setup_discussion
-
create_discussion
-
sign_in patrick
-
redirect_to discussion_url(create_discussion)
-
end
-
-
def setup_multiple_discussions
-
sign_in patrick
-
create_discussion
-
create_public_discussion
-
redirect_to discussion_url(create_discussion)
-
end
-
-
def setup_discussion_as_guest
-
group = FactoryBot.create :group, group_privacy: 'secret'
-
discussion = FactoryBot.build :discussion, group: group, title: "Dirty Dancing Shoes"
-
DiscussionService.create(discussion: discussion, actor: discussion.group.creator)
-
discussion.add_guest!(jennifer, discussion.author)
-
sign_in jennifer
-
-
redirect_to discussion_url(discussion)
-
end
-
-
def setup_forkable_discussion
-
create_discussion
-
create_another_discussion
-
sign_in patrick
-
CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "This is totally on topic!"), actor: jennifer)
-
event = CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "This is totally **off** topic!"), actor: jennifer)
-
CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "This is a reply to the off-topic thing!", parent: event.eventable), actor: emilio)
-
CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "This is also off-topic"), actor: emilio)
-
CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "This is totally back on topic!"), actor: patrick)
-
-
redirect_to discussion_url(create_discussion)
-
end
-
-
def setup_thread_catch_up
-
jennifer.update(email_catch_up_day: 7)
-
CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "first comment"), actor: patrick)
-
event = CommentService.create(comment: FactoryBot.create(:comment, discussion: create_discussion, body: "removed comment"), actor: patrick)
-
CommentService.discard(comment: event.eventable, actor: event.user)
-
DiscussionService.update(discussion: create_discussion,
-
params: {recipient_message: 'this is an edit message'},
-
actor: patrick)
-
poll = fake_poll
-
PollService.create(poll: poll, actor: patrick)
-
create_fake_stances(poll: poll)
-
PollService.update(poll: poll, actor: patrick, params: {recipient_message: 'updated the poll here <br> newline'})
-
DiscussionService.close(discussion: create_discussion, actor: patrick)
-
UserMailer.catch_up(jennifer.id, 1.hour.ago).deliver_now
-
last_email
-
end
-
-
def setup_unread_discussion
-
read = Comment.new(discussion: create_discussion, body: "Here is some read content")
-
unread = Comment.new(discussion: create_discussion, body: "Here is some unread content")
-
another_unread = Comment.new(discussion: create_discussion, body: "Here is some more unread content")
-
sign_in patrick
-
-
CommentService.create(comment: read, actor: patrick)
-
CommentService.create(comment: unread, actor: jennifer)
-
CommentService.create(comment: another_unread, actor: jennifer)
-
redirect_to discussion_url(create_discussion)
-
end
-
-
def setup_discussion_for_jennifer
-
sign_in jennifer
-
redirect_to discussion_url(create_discussion)
-
end
-
-
def setup_open_and_closed_discussions
-
create_discussion
-
create_closed_discussion
-
sign_in patrick
-
patrick.update(experiences: { closingThread: true })
-
redirect_to group_url(create_group)
-
end
-
-
def setup_pages_of_closed_discussions
-
@group = saved(fake_group)
-
@group.add_admin!(patrick)
-
sign_in patrick
-
60.times do
-
saved(fake_discussion(group: @group, closed_at: 5.days.ago))
-
end
-
redirect_to group_url(@group)
-
end
-
-
def setup_comment_with_versions
-
comment = Comment.new(discussion: create_discussion, body: "What star sign are you?")
-
CommentService.create(comment: comment, actor: jennifer)
-
comment.update(body: "What moon sign are you?")
-
comment.update_versions_count
-
sign_in patrick
-
redirect_to discussion_url(create_discussion)
-
end
-
-
def setup_discussion_with_versions
-
create_discussion
-
create_discussion.update(title: "What moon sign are you?")
-
create_discussion.update_versions_count
-
sign_in patrick
-
redirect_to discussion_url(create_discussion)
-
end
-
-
# discussion mailer emails
-
-
def setup_discussion_mailer_discussion_created_email
-
sign_in jennifer
-
@group = FactoryBot.create(:group, name: "Girdy Dancing Shoes", creator: patrick)
-
@group.add_admin! patrick
-
@group.add_member! jennifer
-
discussion = FactoryBot.build(:discussion, title: "Let's go to the moon!", group: @group)
-
discussion.files.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'images', 'strongbad.png')),
-
filename: 'strongbad.png',
-
content_type: 'image/jpeg')
-
-
DiscussionService.create(discussion: discussion, actor: patrick, params: {recipient_user_ids: [jennifer.id]})
-
last_email
-
end
-
-
def setup_discussion_mailer_discussion_edited_email
-
sign_in jennifer
-
@group = FactoryBot.create(:group, name: "Girdy Dancing Shoes", creator: patrick)
-
@group.add_admin! patrick
-
@group.add_member! jennifer
-
discussion = FactoryBot.build(:discussion, title: "Let's go to the moon!", group: @group)
-
DiscussionService.create(discussion: discussion, actor: patrick)
-
DiscussionService.update(discussion: discussion, actor: patrick, params: {recipient_user_ids: [jennifer.id], recipient_message: 'change message & ampersand <yo>! '})
-
last_email
-
end
-
-
def setup_discussion_mailer_discussion_announced_email
-
sign_in jennifer
-
@group = FactoryBot.create(:group, name: "Girdy Dancing Shoes", creator: patrick)
-
@group.add_admin! patrick
-
@group.add_member! jennifer
-
discussion = FactoryBot.build(:discussion, title: "Let's go to the moon!", group: @group)
-
event = DiscussionService.create(discussion: discussion, actor: patrick)
-
DiscussionService.invite(discussion: discussion, actor: patrick, params: {recipient_user_ids: [jennifer.id]})
-
last_email
-
end
-
-
def setup_discussion_mailer_invitation_created_email
-
group = FactoryBot.create(:group, name: "Dirty Dancing Shoes", creator: patrick)
-
group.add_admin! patrick
-
discussion = FactoryBot.build(:discussion, title: "Let's go to the moon!", group: group)
-
event = DiscussionService.create(discussion: discussion, actor: patrick)
-
comment = FactoryBot.build(:comment, discussion: discussion)
-
CommentService.create(comment: comment, actor: patrick)
-
DiscussionService.invite(discussion: discussion, actor: patrick, params: {recipient_emails: 'jen@example.com'})
-
last_email
-
end
-
-
def setup_discussion_mailer_new_comment_email
-
@group = Group.create!(name: 'Dirty Dancing Shoes')
-
@group.add_admin!(patrick).set_volume!(:loud)
-
@group.add_member! jennifer
-
-
@discussion = Discussion.new(title: 'What star sign are you?',
-
group: @group,
-
description: "Wow, what a __great__ day.",
-
author: jennifer)
-
DiscussionService.create(discussion: @discussion, actor: @discussion.author)
-
@comment = Comment.new(author: jennifer, body: "hello _patrick_.", discussion: @discussion)
-
CommentService.create(comment: @comment, actor: jennifer)
-
last_email
-
end
-
-
def setup_discussion_mailer_comment_replied_to_email
-
@group = Group.create!(name: 'Dirty Dancing Shoes')
-
@group.add_admin!(patrick)
-
@group.add_member! jennifer
-
-
-
@discussion = Discussion.new(title: 'What star sign are you?',
-
group: @group,
-
description: "Wow, what a __great__ day.",
-
author: jennifer)
-
DiscussionService.create(discussion: @discussion, actor: @discussion.author)
-
@comment = Comment.new(body: "hello _patrick.", discussion: @discussion)
-
CommentService.create(comment: @comment, actor: jennifer)
-
@reply_comment = Comment.new(body: "why, hello there @#{jennifer.username}", parent: @comment, discussion: @discussion)
-
CommentService.create(comment: @reply_comment, actor: patrick)
-
last_email
-
end
-
-
def setup_discussion_mailer_user_mentioned_email
-
@group = saved fake_group
-
GroupService.create(group: @group, actor: patrick)
-
-
@group.add_member! jennifer
-
@discussion = fake_discussion(group: @group, description: "hey @#{patrick.username} wanna dance?")
-
DiscussionService.create(discussion: @discussion, actor: jennifer)
-
last_email
-
end
-
-
def setup_task_reminder_email
-
@group = Group.create!(name: 'Dirty Dancing Shoes')
-
@group.add_admin!(patrick)
-
jennifer.update(time_zone: "Pacific/Auckland")
-
@group.add_member! jennifer
-
datestr = "2021-06-16"
-
-
@discussion = Discussion.new(title: 'time to do your chores!',
-
description_format: 'html',
-
group: @group,
-
description: "<li data-uid='123' data-type='taskItem' data-due-on='#{datestr}' data-remind='1'>this is a task for <span data-mention-id='#{jennifer.username}'>#{jennifer.name}</span></li>",
-
author: jennifer)
-
DiscussionService.create(discussion: @discussion, actor: @discussion.author)
-
expected_remind_at = "{datestr} 06:00".in_time_zone("Pacific/Auckland") - 1.day
-
TaskService.send_task_reminders(expected_remind_at)
-
last_email
-
end
-
end
-
module Dev::Scenarios::EmailSettings
-
def email_settings_as_logged_in_user
-
create_group
-
sign_in patrick
-
redirect_to email_preferences_url(unsubscribe_token: patrick.unsubscribe_token)
-
end
-
-
def email_settings_as_restricted_user
-
create_group
-
redirect_to email_preferences_url(unsubscribe_token: patrick.unsubscribe_token)
-
end
-
end
-
module Dev::Scenarios::Group
-
def setup_group_super_admin
-
patrick.update(is_admin: true)
-
sign_in patrick
-
create_group.add_member! emilio
-
redirect_to group_url(create_group)
-
end
-
-
def setup_group
-
sign_in patrick
-
create_group.add_member! emilio
-
redirect_to group_url(create_group)
-
end
-
-
def setup_group_with_received_email
-
sign_in patrick
-
create_group.add_member! emilio
-
5.times do
-
name = Faker::Name.name
-
email = ReceivedEmail.create(
-
body_html: "<html><body>hello everyone.</body></html>",
-
dkim_valid: true,
-
spf_valid: true,
-
headers: {
-
from: "\"#{name}\" <#{Faker::Internet.email(name: name)}>",
-
to: create_group.handle + "@#{ENV['REPLY_HOSTNAME']}",
-
subject: Faker::TvShows::TheFreshPrinceOfBelAir.quote
-
}
-
)
-
end
-
ReceivedEmailService.route_all
-
redirect_to group_emails_url(create_group)
-
end
-
-
def setup_group_with_max_members
-
sign_in patrick
-
create_group.subscription.update(max_members: 4)
-
redirect_to group_memberships_url(create_group)
-
end
-
-
def setup_trial_group_with_received_email
-
sign_in patrick
-
create_group.subscription.update(plan: 'trial')
-
create_group.add_member! emilio
-
5.times do
-
name = Faker::Name.name
-
email = ReceivedEmail.create(
-
body_html: "<html><body>hello everyone.</body></html>",
-
dkim_valid: true,
-
spf_valid: true,
-
headers: {
-
from: "\"#{name}\" <#{Faker::Internet.email(name: name)}>",
-
to: create_group.handle + "@#{ENV['REPLY_HOSTNAME']}",
-
subject: Faker::TvShows::TheFreshPrinceOfBelAir.quote
-
}
-
)
-
end
-
ReceivedEmailService.route_all
-
redirect_to group_emails_url(create_group)
-
end
-
-
def setup_user_no_group
-
sign_in patrick
-
redirect_to dashboard_url
-
end
-
-
def setup_group_with_discussion
-
sign_in patrick
-
create_group.add_member! emilio
-
create_discussion
-
redirect_to group_url(create_group)
-
end
-
-
def setup_group_with_handle
-
sign_in patrick
-
group = create_group
-
group.update(name: 'Ghostbusters', handle: 'ghostbusters')
-
redirect_to group_url(group)
-
end
-
-
def setup_group_with_pending_invitations
-
sign_in patrick
-
create_group
-
other_invite = FactoryBot.create(:user, name: nil, email: "hidden@test.com")
-
my_invite = FactoryBot.create(:user, name: nil, email: "shown@test.com")
-
FactoryBot.create :membership, group: create_group, accepted_at: nil, inviter: jennifer, user: other_invite
-
FactoryBot.create :membership, group: create_group, accepted_at: nil, inviter: patrick, user: my_invite
-
redirect_to group_url(create_group)
-
end
-
-
def visit_group_as_subgroup_member
-
sign_in jennifer
-
create_subgroup.add_member! jennifer
-
another_create_subgroup.add_member! jennifer
-
redirect_to group_url(create_another_group)
-
end
-
-
def setup_group_with_subgroups
-
sign_in jennifer
-
create_another_group.add_member! jennifer
-
create_subgroup.add_member! jennifer
-
another_create_subgroup
-
redirect_to group_url(create_another_group)
-
end
-
-
def setup_group_with_subgroups_as_admin
-
sign_in jennifer
-
create_another_group.add_admin! jennifer
-
create_subgroup.add_member! jennifer
-
create_subgroup.add_member! fake_user name: 'only in subgroup'
-
another_create_subgroup
-
redirect_to group_url(create_subgroup)
-
end
-
-
def setup_subgroup_with_parent_member_visibility
-
sign_in patrick
-
@group = Group.create!(name: 'Closed Dirty Dancing Shoes',
-
group_privacy: 'closed')
-
@group.add_admin! jennifer
-
@group.add_member! jennifer
-
@group.add_member! patrick
-
@subgroup = Group.create!(name: 'Johnny Utah',
-
parent: @group,
-
discussion_privacy_options: 'public_or_private',
-
parent_members_can_see_discussions: true,
-
group_privacy: 'closed', creator: jennifer)
-
discussion = FactoryBot.create :discussion, group: @subgroup, title: "Vaya con dios", private: true, author: jennifer
-
DiscussionService.create(discussion: discussion, actor: discussion.author)
-
redirect_to group_url(@subgroup)
-
end
-
-
def setup_group_with_subgroups_as_admin_landing_in_other_subgroup
-
sign_in jennifer
-
create_another_group.add_admin! jennifer
-
create_subgroup.add_member! jennifer
-
another_create_subgroup
-
redirect_to group_url(another_create_subgroup)
-
end
-
-
def setup_open_group
-
@group = Group.create!(name: 'Open Dirty Dancing Shoes',
-
group_privacy: 'open')
-
@group.add_admin! patrick
-
@group.add_member! jennifer
-
membership = Membership.find_by(user: patrick, group: @group)
-
sign_in patrick
-
redirect_to group_url(create_group)
-
end
-
-
def setup_closed_group
-
@group = Group.create!(name: 'Closed Dirty Dancing Shoes', group_privacy: 'closed')
-
@group.add_admin! patrick
-
@group.add_member! jennifer
-
membership = Membership.find_by(user: patrick, group: @group)
-
sign_in patrick
-
redirect_to group_url(create_group)
-
end
-
-
def setup_secret_group
-
@group = Group.create!(name: 'Secret Dirty Dancing Shoes', handle: 'secret-shoes', group_privacy: 'secret')
-
@group.add_admin! patrick
-
@group.add_member! jennifer
-
membership = Membership.find_by(user: patrick, group: @group)
-
sign_in patrick
-
redirect_to group_url(create_group)
-
end
-
-
def setup_group_with_multiple_coordinators
-
create_group.add_admin! emilio
-
sign_in patrick
-
redirect_to group_url(create_group)
-
end
-
-
def setup_group_with_no_coordinators
-
create_group
-
@group.admin_memberships.each{|m| m.update(admin: false)}
-
sign_in patrick
-
redirect_to group_url(create_group)
-
end
-
-
def setup_group_with_restrictive_settings
-
sign_in max
-
create_stance
-
create_discussion
-
create_group.update(
-
members_can_add_members: false,
-
members_can_edit_discussions: false,
-
members_can_edit_comments: false,
-
members_can_raise_motions: false,
-
members_can_start_discussions: false,
-
members_can_create_subgroups: false
-
)
-
create_group.add_member! max
-
redirect_to group_url create_group
-
end
-
-
def view_open_group_as_non_member
-
sign_in patrick
-
@group = Group.create!(name: 'Open Dirty Dancing Shoes', membership_granted_upon: 'request', group_privacy: 'open')
-
@group.add_admin! jennifer
-
@discussion = Discussion.new(title: "I carried a watermelon", private: false, author: jennifer, group: @group)
-
DiscussionService.create(discussion: @discussion, actor: jennifer)
-
CommentService.create(comment: Comment.new(body: "It was real seedy", discussion: @discussion), actor: jennifer)
-
redirect_to group_url(create_group)
-
end
-
-
def view_open_group_as_visitor
-
@group = Group.create!(name: 'Open Dirty Dancing Shoes',
-
membership_granted_upon: 'request',
-
group_privacy: 'open')
-
@group.add_admin! jennifer
-
@discussion = Discussion.new(title: 'I carried a watermelon', private: false, author: jennifer, group: @group)
-
DiscussionService.create(discussion: @discussion, actor: @discussion.author)
-
redirect_to group_url(@group)
-
end
-
-
def setup_start_thread_form_from_url
-
sign_in patrick
-
redirect_to "/d/new/?group_id=#{create_group.id}&title=testing title&type=thread"
-
end
-
-
def setup_start_poll_form_from_url
-
sign_in patrick
-
redirect_to "/p/new/count?group_id=#{create_group.id}&title=testing title"
-
end
-
end
-
module Dev::Scenarios::Inbox
-
def setup_inbox
-
sign_in patrick
-
recent_discussion group: create_another_group
-
old_discussion; pinned_discussion
-
redirect_to inbox_url
-
end
-
end
-
module Dev::Scenarios::JoinGroup
-
def setup_public_group_to_join_upon_request
-
sign_in jennifer
-
create_another_group.update(group_privacy: 'open')
-
create_another_group.update(membership_granted_upon: 'request')
-
create_public_discussion
-
redirect_to group_url(create_another_group)
-
end
-
-
def setup_closed_group_to_join
-
sign_in jennifer
-
create_another_group
-
create_public_discussion
-
private_create_discussion
-
create_subgroup
-
redirect_to group_url(create_another_group)
-
end
-
end
-
module Dev::Scenarios::Membership
-
def setup_group_as_member
-
create_group.update_admin_memberships_count
-
sign_in jennifer
-
redirect_to group_url(create_group)
-
end
-
-
def setup_membership_with_title
-
sign_in patrick
-
create_group.memberships.find_by(user: patrick).update(title: "Suzerain!")
-
redirect_to group_url(create_group)
-
end
-
end
-
module Dev::Scenarios::MembershipRequest
-
def setup_membership_requests
-
sign_in patrick
-
create_group
-
3.times do
-
request = MembershipRequest.new(group: create_group, introduction: "I'd like to make decisions with y'all")
-
MembershipRequestService.create(membership_request: request, actor: saved(fake_user))
-
end
-
redirect_to group_url(create_group)
-
end
-
end
-
module Dev::Scenarios::Notification
-
def setup_all_activity_items
-
create_discussion
-
sign_in patrick
-
create_all_activity_items
-
redirect_to discussion_url(create_discussion)
-
end
-
-
def setup_all_notifications
-
sign_in patrick
-
create_all_notifications
-
redirect_to discussion_url(create_discussion)
-
end
-
end
-
module Dev::Scenarios::Profile
-
def setup_restricted_profile
-
sign_in patrick
-
create_group = Group.create!(name: 'Secret Dirty Dancing Shoes',
-
group_privacy: 'secret')
-
create_group.add_member!(jennifer)
-
redirect_to "/u/#{jennifer.username}"
-
end
-
-
def setup_profile_with_group_visible_to_members
-
sign_in patrick
-
create_group = Group.create!(name: 'Secret Dirty Dancing Shoes',
-
group_privacy: 'secret')
-
create_group.add_admin!(patrick)
-
create_group.add_member!(jennifer)
-
redirect_to "/u/#{jennifer.username}"
-
end
-
-
def setup_deactivated_user
-
patrick.update(deactivated_at: 1.day.ago)
-
redirect_to "/dashboard"
-
end
-
-
def setup_user_reactivation_email
-
patrick.update(deactivated_at: 1.day.ago)
-
UserService.reactivate(patrick.id)
-
last_email
-
end
-
end
-
module Dev::Scenarios::Tags
-
def setup_discussion_with_tag
-
tag = Tag.create(name: "Tag Name", color: "#cccccc", group: create_discussion.group)
-
sign_in patrick
-
redirect_to discussion_url(create_discussion)
-
end
-
-
def setup_inbox_with_tag
-
tag = Tag.create(name: "Tag Name", color: "#cccccc", group: create_discussion.group)
-
discussion_tag = DiscussionTag.create(discussion: create_discussion, tag: tag)
-
sign_in patrick
-
redirect_to inbox_url
-
end
-
-
def view_discussion_as_visitor_with_tags
-
group = Group.create!(name: 'Open Dirty Dancing Shoes', group_privacy: 'open')
-
group.add_admin! patrick
-
discussion = group.discussions.create!(title: 'This thread is public', private: false, author: patrick)
-
DiscussionService.create(discussion: discussion, actor: discussion.author)
-
tag = group.tags.create(name: "Tag Name", color: "#cccccc")
-
discussion_tag = discussion.discussion_tags.create(tag: tag)
-
redirect_to discussion_url(discussion)
-
end
-
-
def visit_tags_page
-
group = Group.create!(name: 'Open Dirty Dancing Shoes', group_privacy: 'open')
-
group.add_admin! patrick
-
discussion = group.discussions.create!(title: 'This thread is public', private: false, author: patrick)
-
DiscussionService.create(discussion: discussion, actor: discussion.author)
-
tag = group.tags.create(name: "Tag Name", color: "#cccccc")
-
discussion_tag = discussion.discussion_tags.create(tag: tag)
-
redirect_to "/g/#{group.key}/tags"
-
end
-
end
-
module Dev::Scenarios::Util
-
def accept_last_invitation
-
membership = Membership.pending.last
-
MembershipService.redeem(membership: invitation, actor: max)
-
redirect_to(group_url(membership.group))
-
end
-
-
def use_last_login_token
-
redirect_to(login_token_url(::LoginToken.last.token))
-
end
-
-
private
-
-
def cleanup_database
-
reset_session
-
::User.delete_all
-
::Group.delete_all
-
::Membership.delete_all
-
::Poll.delete_all
-
::Outcome.delete_all
-
::Event.delete_all
-
::Discussion.delete_all
-
::Stance.delete_all
-
::StanceChoice.delete_all
-
::PollOption.delete_all
-
::Task.delete_all
-
::DiscussionReader.delete_all
-
::DiscussionTemplate.delete_all
-
::PollTemplate.delete_all
-
::ActionMailer::Base.deliveries = []
-
end
-
end
-
class DirectUploadsController < ActiveStorage::DirectUploadsController
-
protect_from_forgery with: :exception
-
skip_before_action :verify_authenticity_token
-
-
private
-
def direct_upload_json(blob)
-
json = blob.as_json(root: false, methods: :signed_id).merge(
-
direct_upload: {
-
url: blob.service_url_for_direct_upload,
-
headers: blob.service_headers_for_direct_upload
-
})
-
-
json.merge!(download_url:
-
Rails.application.routes.url_helpers.rails_blob_path(blob, only_path: true)
-
)
-
-
if blob.representable?
-
json.merge!(preview_url:
-
Rails.application.routes.url_helpers.rails_representation_path(
-
blob.representation(HasRichText::PREVIEW_OPTIONS),
-
only_path: true
-
)
-
)
-
end
-
json
-
end
-
end
-
1
class DiscussionsController < ApplicationController
-
end
-
1
class EmailActionsController < AuthenticateByUnsubscribeTokenController
-
1
def unfollow_discussion
-
2
discussion_reader = DiscussionReader.for(discussion: discussion, user: user)
-
-
2
if ['normal', 'quiet'].include?(params[:new_volume])
-
discussion_reader.set_volume!(params[:new_volume].to_sym)
-
else
-
2
if discussion_reader.volume_is_loud?
-
1
discussion_reader.set_volume! :normal
-
else
-
1
discussion_reader.set_volume! :quiet
-
end
-
end
-
-
2
redirect_to root_path, notice: t(:"email_actions.unfollowed_discussion", thread_title: discussion.title)
-
end
-
-
1
def mark_discussion_as_read
-
3
GenericWorker.perform_async(
-
'DiscussionService',
-
'mark_as_read_simple_params',
-
discussion.id,
-
event.sequence_id || [],
-
user.id,
-
)
-
2
event.notifications.where(user: user).update_all(viewed: true)
-
2
respond_with_pixel
-
rescue ActiveRecord::RecordNotFound
-
1
respond_with_pixel
-
end
-
-
1
def mark_notification_as_read
-
1
Notification.find_by!(id: params[:id], user_id: user.id).update(viewed: true)
-
1
respond_with_pixel
-
rescue ActiveRecord::RecordNotFound
-
respond_with_pixel
-
end
-
-
1
def mark_summary_email_as_read
-
1
GenericWorker.perform_async('DiscussionService', 'mark_summary_email_as_read', user.id, params[:time_start].to_i, params[:time_finish].to_i)
-
-
1
respond_to do |format|
-
1
format.html {
-
flash[:notice] = I18n.t "email.catch_up.marked_as_read_success"
-
redirect_to root_path
-
}
-
2
format.gif { respond_with_pixel }
-
end
-
end
-
-
1
private
-
-
1
def respond_with_pixel
-
5
send_file Rails.root.join('app','assets','images', 'empty.gif'), type: 'image/gif', disposition: 'inline'
-
end
-
-
1
def discussion
-
7
@discussion ||= user.discussions.find(params[:discussion_id])
-
end
-
-
1
def event
-
4
@event ||= Event.find params[:event_id]
-
end
-
end
-
1
class GroupsController < ApplicationController
-
1
def index
-
1
@groups = Queries::ExploreGroups.new.search_for(params[:q]).order('groups.memberships_count DESC')
-
1
@total = @groups.count
-
1
limit = params.fetch(:limit, 50)
-
1
if @total < limit
-
1
@pages = 1
-
else
-
if @total % limit > 0
-
@pages = @total / limit + 1
-
else
-
@pages = @total / limit
-
end
-
end
-
1
@page = params.fetch(:page, 1).to_i.clamp(1, @pages)
-
1
@offset = @page == 1 ? 0 : ((@page - 1) * limit)
-
1
@groups = @groups.limit(limit).offset(@offset)
-
end
-
-
1
def export
-
3
@exporter = GroupExporter.new(load_and_authorize(:group, :export))
-
1
respond_to do |format|
-
1
format.html
-
end
-
end
-
end
-
class HelpController < ApplicationController
-
def markdown
-
render layout: false
-
end
-
-
def api
-
render layout: 'basic'
-
end
-
-
def api2
-
current_user.save if current_user.api_key_changed?
-
@group_id = params[:group_id] || 123
-
@api_key = current_user.api_key
-
render layout: 'basic'
-
end
-
-
def api3
-
render layout: 'basic'
-
end
-
end
-
class Identities::BaseController < ApplicationController
-
def oauth
-
session[:back_to] = params[:back_to] || request.referrer
-
redirect_to oauth_url
-
end
-
-
def create
-
if identity.save
-
associate_identity
-
redirect_to session.delete(:back_to) || dashboard_path
-
else
-
respond_with_error message: "Could not connect to #{controller_name}!"
-
end
-
end
-
-
def destroy
-
if i = current_user.identities.find_by(identity_type: controller_name)
-
i.destroy
-
redirect_to request.referrer || root_path
-
else
-
respond_with_error message: "Not connected to #{controller_name}!"
-
end
-
end
-
-
private
-
-
def client
-
@client ||= "Clients::#{controller_name.classify}".constantize.instance
-
end
-
-
def redirect_uri
-
send :"#{controller_name}_authorize_url"
-
end
-
-
def identity
-
@identity ||= identity_class.new(identity_params).tap { |i| complete_identity(i) }
-
end
-
-
def existing_identity
-
@existing_identity ||= identity_class.with_user.find_by(
-
identity_type: identity.identity_type,
-
uid: identity.uid
-
)
-
end
-
-
def existing_user
-
@existing_user ||= User.verified.find_by(email: identity.email)
-
end
-
-
def associate_identity
-
if user = existing_identity&.user || current_user.presence || existing_user
-
user.associate_with_identity(identity)
-
sign_in(user)
-
flash[:notice] = t(:'devise.sessions.signed_in')
-
else
-
session[:pending_identity_id] = identity.tap(&:save).id
-
end
-
end
-
-
# override with differing ways to fetch the access token from the response
-
def identity_params
-
{ access_token: client.fetch_access_token(params[:code], redirect_uri).json['access_token'] }
-
end
-
-
# override with additional follow-up API calls if they're needed to gather more info
-
# (such as logo url, user name, etc)
-
def complete_identity(i)
-
i.fetch_user_info
-
end
-
-
def identity_class
-
"Identities::#{controller_name.classify}".constantize
-
end
-
-
def oauth_url
-
"#{oauth_host}?#{oauth_params.to_query}"
-
end
-
-
def oauth_host
-
raise NotImplementedError.new
-
end
-
-
def oauth_params
-
{ client.client_key_name => client.key, redirect_uri: redirect_uri, scope: oauth_scope }
-
end
-
-
def oauth_client_id_field
-
:client_id
-
end
-
-
def oauth_scope
-
client.scope.join(',')
-
end
-
end
-
class Identities::FacebookController < Identities::BaseController
-
before_action :allow_facebook_domains, only: :webview
-
layout false
-
-
def verify
-
render text: params[:"hub.challenge"]
-
end
-
-
def webhook
-
Clients::Facebook.instance.post_poll_button(recipient_id)
-
head :ok
-
end
-
-
def webview
-
end
-
-
private
-
-
def recipient_id
-
params.dig(:entry, 0, :messaging, 0, :sender, :id)
-
end
-
-
def allow_facebook_domains
-
response.headers['X-FRAME-OPTIONS'] = 'ALLOW_FROM *'
-
end
-
-
def complete_identity(identity)
-
super
-
identity.fetch_user_avatar
-
end
-
-
def oauth_host
-
"https://www.facebook.com/v2.8/dialog/oauth"
-
end
-
end
-
class Identities::GoogleController < Identities::BaseController
-
-
private
-
-
def oauth_url
-
super.gsub("%2B", "+")
-
end
-
-
def oauth_host
-
"https://accounts.google.com/o/oauth2/v2/auth"
-
end
-
-
def oauth_params
-
super.merge(response_type: :code, scope: client.scope.join('+'))
-
end
-
end
-
class Identities::NextcloudController < Identities::BaseController
-
-
private
-
-
def oauth_host
-
ENV['NEXTCLOUD_HOST']
-
end
-
-
def oauth_url
-
"#{oauth_host}#{oauth_authorize_path}?#{oauth_params.to_query}"
-
end
-
-
def oauth_authorize_path
-
'/index.php/apps/oauth2/authorize'.freeze
-
end
-
-
def oauth_params
-
{ client.client_key_name => client.key, redirect_uri: redirect_uri, response_type: :code }
-
end
-
end
-
class Identities::OauthController < Identities::BaseController
-
-
private
-
-
def oauth_url
-
"#{oauth_auth_url}?#{oauth_params.to_query}"
-
end
-
-
def oauth_auth_url
-
ENV.fetch('OAUTH_AUTH_URL')
-
end
-
-
def oauth_params
-
{ client.client_key_name => client.key, redirect_uri: redirect_uri, scope: ENV.fetch('OAUTH_SCOPE'), response_type: :code }
-
end
-
end
-
class Identities::SamlController < Identities::BaseController
-
skip_before_action :verify_authenticity_token
-
-
def metadata
-
meta = OneLogin::RubySaml::Metadata.new
-
render :xml => meta.generate(sp_settings), :content_type => "application/samlmetadata+xml"
-
end
-
-
private
-
-
def identity
-
@identity ||= identity_class.new.tap do |i|
-
i.response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], skip_recipient_check: true)
-
i.response.settings = i.settings
-
complete_identity(i)
-
end
-
end
-
-
def oauth_url
-
OneLogin::RubySaml::Authrequest.new.create(Identities::Saml.new.settings)
-
end
-
-
def identity_params
-
{ response: params[:SAMLResponse] }
-
end
-
-
def sp_settings
-
# this is just for testing purposes.
-
# should retrieve SAML-settings based on subdomain, IP-address, NameID or similar
-
settings = OneLogin::RubySaml::Settings.new
-
-
# When disabled, saml validation errors will raise an exception.
-
settings.soft = true
-
-
#SP section
-
settings.issuer = saml_metadata_url
-
settings.assertion_consumer_service_url = saml_oauth_callback_url
-
settings.assertion_consumer_logout_service_url = saml_unauthorize_url
-
-
# onelogin_app_id = "<onelogin-app-id>"
-
#
-
# # IdP section
-
# settings.idp_entity_id = "https://app.onelogin.com/saml/metadata/#{onelogin_app_id}"
-
# settings.idp_sso_target_url = "https://app.onelogin.com/trust/saml2/http-redirect/sso/#{onelogin_app_id}"
-
# settings.idp_slo_target_url = "https://app.onelogin.com/trust/saml2/http-redirect/slo/#{onelogin_app_id}"
-
# settings.idp_cert = ""
-
-
-
# or settings.idp_cert_fingerprint = ""
-
# settings.idp_cert_fingerprint_algorithm = XMLSecurity::Document::SHA1
-
-
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
-
-
# Security section
-
settings.security[:authn_requests_signed] = false
-
settings.security[:logout_requests_signed] = false
-
settings.security[:logout_responses_signed] = false
-
settings.security[:metadata_signed] = false
-
settings.security[:digest_method] = XMLSecurity::Document::SHA1
-
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1
-
-
settings
-
end
-
end
-
1
class LoginTokensController < ApplicationController
-
-
1
def show
-
2
session[:pending_login_token] = login_token.token
-
2
redirect_to login_token.redirect || dashboard_path
-
end
-
-
1
private
-
-
1
def login_token
-
4
@login_token ||= LoginToken.find_by!(token: params[:token])
-
end
-
end
-
1
class ManifestController < ApplicationController
-
1
respond_to :json
-
-
1
ICON_SIZES = %w(32 48 128 144 192 512).freeze
-
-
1
def show
-
1
render json: {
-
name: AppConfig.theme[:site_name],
-
short_name: AppConfig.theme[:site_name],
-
display: 'standalone',
-
orientation: 'portrait',
-
start_url: '/dashboard',
-
background_color: AppConfig.theme[:primary_color],
-
theme_color: AppConfig.theme[:text_on_primary_color],
-
6
icons: ICON_SIZES.map { |size| icon_for(size) }
-
}
-
end
-
-
1
private
-
-
1
def icon_for(size)
-
{
-
6
src: [root_url.chomp('/'), AppConfig.theme[:"icon#{size}_src"]].join(''),
-
sizes: "#{size}x#{size}",
-
type: "image/png"
-
}
-
end
-
end
-
1
class MembershipsController < ApplicationController
-
1
include PrettyUrlHelper
-
-
1
def join
-
1
group = Group.published.find_by!(token: params.require(:token))
-
1
session[:pending_group_token] = group.token
-
1
redirect_to back_to_url || polymorphic_url(group)
-
end
-
-
1
def show
-
8
session[:pending_membership_token] = membership.token
-
6
redirect_to back_to_url || polymorphic_url(Group.find_by(id: membership.group_id))
-
rescue ActiveRecord::RecordNotFound
-
2
redirect_to join_url(Group.find_by!(token: params[:token]))
-
end
-
-
1
def consume
-
4
head :ok
-
end
-
-
1
private
-
-
1
def membership
-
14
@membership ||= Membership.find_by!(token: params[:token])
-
end
-
-
1
def back_to_url
-
7
@back_to_url ||= begin
-
7
url = URI.decode_www_form_component params[:back_to].to_s
-
7
url if url.match(/^http[s]?:\/\/#{ENV['CANONICAL_HOST']}/)
-
end
-
end
-
end
-
1
class MergeUsersController < ApplicationController
-
1
layout 'basic'
-
-
1
def confirm
-
@source_user = User.active.find_by!(id: params[:source_id])
-
@target_user = User.active.find_by!(id: params[:target_id])
-
@hash = params[:hash]
-
if MergeUsersService.validate(source_user: @source_user, target_user: @target_user, hash: @hash)
-
render :confirm
-
else
-
respond_with_error(status: 422)
-
end
-
end
-
-
1
def merge
-
2
@source_user = User.active.find_by!(id: params[:source_id])
-
2
@target_user = User.active.find_by!(id: params[:target_id])
-
2
@hash = params[:hash]
-
2
if MergeUsersService.validate(source_user: @source_user, target_user: @target_user, hash: @hash)
-
1
MigrateUserWorker.perform_async(@source_user.id, @target_user.id)
-
1
render :complete
-
else
-
1
respond_with_error(status: 422)
-
end
-
end
-
-
end
-
require_relative Rails.root.join('lib/pie_chart')
-
-
class PieChartController < ApplicationController
-
def show
-
scores = params[:scores].to_s.split(',').map(&:to_i)
-
colors = params[:colors].to_s.split(',').map {|c| "##{c}"}
-
-
svg = PieChartSVG.from_primitives(scores, colors)
-
-
name = scores.join() + colors.join()
-
-
file = Tempfile.new('name')
-
file.write(svg.render)
-
file.rewind
-
-
png = ImageProcessing::MiniMagick
-
.source(file)
-
.convert("png")
-
.call
-
-
file.unlink
-
-
send_file(png, type: 'image/png', disposition: 'inline')
-
end
-
end
-
class PollTemplatesController < ApplicationController
-
# TODO remove this file
-
def dump_i18n
-
group = load_and_authorize :group, :export
-
templates = {}
-
PollTemplate.where(group_id: group.id).order(:position).each do |pt|
-
templates = templates.merge(pt.dump_i18n)
-
end
-
render plain: templates.to_yaml, layout: false, template: nil
-
end
-
end
-
1
class PollsController < ApplicationController
-
-
1
include UsesMetadata
-
1
include LoadAndAuthorize
-
1
include EmailHelper
-
-
1
helper :email
-
-
1
def export
-
3
@exporter = PollExporter.new(load_and_authorize(:poll, :export))
-
2
@recipient = current_user
-
2
@action_name = :export
-
-
2
respond_to do |format|
-
2
format.html
-
3
format.csv { send_data @exporter.to_csv, filename:@exporter.file_name }
-
end
-
end
-
-
1
def example
-
if poll = PollGenerator.new(params[:type]).generate!
-
redirect_to poll
-
else
-
redirect_to root_path, notice: "Sorry, we don't know about that poll type"
-
end
-
end
-
-
1
private
-
-
1
def current_user
-
restricted_user || super
-
end
-
end
-
1
class ReceivedEmailsController < ApplicationController
-
1
skip_before_action :verify_authenticity_token
-
-
1
def create
-
13
email = build_received_email_from_params
-
-
13
if email.is_addressed_to_loomio? && !email.is_auto_response?
-
13
email.save!
-
13
ReceivedEmailService.route(email)
-
end
-
-
13
head :ok
-
end
-
-
1
private
-
-
1
def build_received_email_from_params
-
13
data = JSON.parse(params[:mailinMsg])
-
-
13
email = ReceivedEmail.new(
-
headers: data['headers'],
-
body_text: data['text'],
-
body_html: data['html'],
-
13
dkim_valid: data['dkim'] == 'pass' ? true : false,
-
13
spf_valid: data['spf'] == 'pass' ? true : false,
-
)
-
-
13
email.attachments = data.fetch('attachments', []).map do |a|
-
{
-
io: StringIO.new(Base64.decode64(params[a['generatedFileName']])),
-
content_type: a['contentType'],
-
filename: a['generatedFileName']
-
}
-
end
-
-
13
email
-
end
-
end
-
1
class RedirectController < ApplicationController
-
1
def group
-
1
redirect
-
end
-
-
1
def discussion
-
1
redirect
-
end
-
-
1
def poll
-
redirect
-
end
-
-
1
private
-
-
1
def redirect(model: action_name, to: ModelLocator.new(model, params).locate)
-
2
if to.present?
-
2
redirect_to send(:"#{model}_url", to), status: :moved_permanently
-
else
-
respond_with_error message: :"errors.not_found", status: 404
-
end
-
end
-
end
-
class RobotsController < ActionController::Base
-
respond_to :text
-
-
def show
-
if ENV['ALLOW_ROBOTS']
-
render :public_robots
-
else
-
render :private_robots
-
end
-
end
-
end
-
class RootController < ApplicationController
-
def index
-
redirect_to dashboard_path
-
end
-
end
-
class ThreadTemplatesController < ApplicationController
-
# TODO remove this file
-
def dump_i18n
-
@group = load_and_authorize(:group, :export)
-
templates = {}
-
DiscussionTemplate.where(group_id: @group.id).order(:position).each do |dt|
-
templates = templates.merge(dt.dump_i18n)
-
end
-
render plain: templates.to_yaml, layout: false, template: nil
-
end
-
end
-
class Users::PasswordsController < Devise::PasswordsController
-
-
def update
-
self.resource = resource_class.reset_password_by_token(resource_params)
-
-
if resource.errors.empty?
-
set_flash_message!(:notice, :updated)
-
sign_in(resource)
-
respond_with resource, location: after_sign_in_path_for(resource)
-
else
-
set_minimum_password_length
-
respond_with resource
-
end
-
end
-
-
private
-
-
def require_no_authentication
-
# noop
-
end
-
end
-
1
class UsersController < ApplicationController
-
1
include UsesMetadata
-
end
-
class AppConfig
-
CONFIG_FILES = %w(
-
webhook_event_kinds
-
colors
-
emojis
-
poll_types
-
poll_templates
-
discussion_templates
-
providers
-
doctypes
-
locales
-
)
-
-
BANNED_CHARS = %(\\s:,;'"`<>)
-
EMAIL_REGEX = /[^#{BANNED_CHARS}]+?@[^#{BANNED_CHARS}]+\.[^#{BANNED_CHARS}]+/
-
URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/
-
-
CONFIG_FILES.each do |config|
-
define_singleton_method(config) do
-
instance_variable_get(:"@#{config}") ||
-
instance_variable_set(:"@#{config}", YAML.load_file(Rails.root.join("config", "#{config}.yml")))
-
end
-
end
-
-
def self.release
-
@release ||= begin
-
(`git rev-parse HEAD`.strip.presence || File.mtime("app").to_i.to_s)
-
end
-
end
-
-
def self.image_regex
-
doctypes.detect { |type| type['name'] == 'image' }['regex']
-
end
-
-
def self.theme
-
-
brand_colors = {
-
gold: "#DCA034",
-
ink: "#293C4A",
-
wellington: "#7F9EA0",
-
sunset: "#E4C2B9",
-
sky: "#658AE7",
-
rock: "#C77C3B",
-
white: "#FFFFFF"
-
}
-
-
# here are some useful variations on these colours
-
# https://maketintsandshades.com/#DCA034,293C4A,7F9EA0,E4C2B9,658AE7,C77C3B
-
-
logo_color = :gold
-
-
{
-
brand_colors: brand_colors,
-
site_name: ENV.fetch('SITE_NAME', 'Loomio'),
-
channels_uri: ENV.fetch('CHANNELS_URI', 'ws://localhost:5000'),
-
terms_url: ENV['TERMS_URL'],
-
privacy_url: ENV['PRIVACY_URL'],
-
canonical_host: ENV['CANONICAL_HOST'],
-
reply_hostname: ENV['REPLY_HOSTNAME'],
-
help_url: ENV.fetch('HELP_URL', 'https://help.loomio.com/'),
-
icon_src: ENV.fetch('THEME_ICON_SRC', "/brand/icon_#{logo_color}_150h.png"),
-
app_logo_src: ENV.fetch('THEME_APP_LOGO_SRC', "/brand/logo_#{logo_color}.svg"),
-
apple_touch_src: ENV.fetch('APPLE_TOUCH_SRC', "/brand/touch_icon_gold.png"),
-
default_group_cover_src: ENV.fetch('THEME_DEFAULT_GROUP_COVER_SRC', '/theme/default_group_cover.png'),
-
saml_login_provider_name: ENV.fetch('SAML_LOGIN_PROVIDER_NAME', 'SAML'),
-
oauth_login_provider_name: ENV.fetch('OAUTH_LOGIN_PROVIDER_NAME', 'OAUTH'),
-
# used in emails
-
email_header_logo_src: ENV.fetch('THEME_EMAIL_HEADER_LOGO_SRC', "/brand/logo_#{logo_color}_96h.png"),
-
email_footer_logo_src: ENV.fetch('THEME_EMAIL_FOOTER_LOGO_SRC', "/brand/logo_#{logo_color}_48h.png"),
-
primary_color: ENV.fetch('THEME_PRIMARY_COLOR', brand_colors[:sky]),
-
accent_color: ENV.fetch('THEME_ACCENT_COLOR', brand_colors[:gold]),
-
text_on_primary_color: ENV.fetch('THEME_TEXT_ON_PRIMARY_COLOR', '#ffffff'),
-
text_on_accent_color: ENV.fetch('THEME_TEXT_ON_ACCENT_COLOR', '#ffffff'),
-
-
vuetify: {
-
primary: ENV.fetch('THEME_COLOR_PRIMARY', brand_colors[:sky]),
-
secondary: ENV.fetch('THEME_COLOR_SECONDARY', brand_colors[:sunset]),
-
accent: ENV.fetch('THEME_COLOR_ACCENT', brand_colors[:gold]),
-
error: ENV.fetch('THEME_COLOR_ERROR', nil),
-
warning: ENV.fetch('THEME_COLOR_WARNING', nil),
-
info: ENV.fetch('THEME_COLOR_INFO', brand_colors[:sky]),
-
success: ENV.fetch('THEME_COLOR_SUCCESS', nil),
-
anchor: ENV.fetch('THEME_COLOR_ANCHOR', brand_colors[:sky])
-
}
-
}
-
end
-
-
def self.app_features
-
{
-
env: Rails.env,
-
subscriptions: !!ENV.fetch('CHARGIFY_API_KEY', false),
-
demos: ENV.fetch('FEATURES_DEMO_GROUPS', false),
-
trials: ENV.fetch('FEATURES_TRIALS', false),
-
trial_days: ENV.fetch('TRIAL_DAYS', nil),
-
new_thread_button: !!ENV.fetch('FEATURES_NEW_THREAD_BUTTON', false),
-
email_login: !ENV['FEATURES_DISABLE_EMAIL_LOGIN'],
-
create_user: !ENV['FEATURES_DISABLE_CREATE_USER'],
-
create_group: !ENV['FEATURES_DISABLE_CREATE_GROUP'],
-
public_groups: !ENV['FEATURES_DISABLE_PUBLIC_GROUPS'],
-
help_link: !ENV['FEATURES_DISABLE_HELP_LINK'],
-
example_content: !ENV['FEATURES_DISABLE_EXAMPLE_CONTENT'],
-
explore_public_groups: ENV.fetch('FEATURES_EXPLORE_PUBLIC_GROUPS', false),
-
template_gallery: ENV.fetch('FEATURES_TEMPLATE_GALLERY', false),
-
show_contact: ENV.fetch('FEATURES_SHOW_CONTACT', false),
-
show_contact_consent: ENV.fetch('FEATURES_SHOW_CONTACT_CONSENT', false),
-
sentry_sample_rate: ENV.fetch('SENTRY_SAMPLE_RATE', 0.1).to_f,
-
hidden_poll_templates: %w[proposal question],
-
transcription: TranscriptionService.available?
-
}
-
end
-
-
def self.json_parse_or_false(name)
-
if ENV[name]
-
JSON.parse(ENV[name])
-
else
-
false
-
end
-
end
-
end
-
1
class Clients::Base
-
1
attr_reader :key
-
-
1
def self.instance
-
2
new(
-
key: ENV["#{name.demodulize.upcase}_APP_KEY"],
-
secret: ENV["#{name.demodulize.upcase}_APP_SECRET"]
-
)
-
end
-
-
1
def initialize(key: nil, secret: nil, token: nil)
-
40
@key = key
-
40
@secret = secret
-
40
@token = token
-
end
-
-
1
def get(path, params: {}, headers: {}, options: {})
-
perform :get, path, params, headers, options.merge(params_field: :query)
-
end
-
-
1
def post(path, params: {}, headers: {}, options: {})
-
38
perform :post, path, params, headers, options.merge(params_field: :body)
-
end
-
-
1
def post_query(path, params: {}, headers: {}, options: {})
-
perform :post, path, params, headers, options.merge(params_field: :query)
-
end
-
-
# make request for initial user information
-
# overwrite if the API has a different endpoint to get a user
-
1
def fetch_user_info
-
get "me"
-
end
-
-
1
def scope_query_param
-
scope.join(',')
-
end
-
-
1
def client_key_name
-
:client_id
-
end
-
-
1
def scope
-
[]
-
end
-
-
1
private
-
-
1
def perform(method, path, params, headers, options)
-
38
options.reverse_merge!(
-
host: default_host,
-
success: default_success,
-
failure: default_failure,
-
is_success: default_is_success
-
)
-
38
Clients::Request.new(method, [options[:host], path].compact.join('/'), {
-
options[:params_field] => params_for(params),
-
:"headers" => headers_for(headers)
-
38
}).tap { |request| request.perform!(options) }
-
end
-
-
1
def params_for(params = {})
-
38
if require_json_payload?
-
38
default_params.merge(params).to_json
-
else
-
default_params.merge(params)
-
end
-
end
-
-
1
def headers_for(headers = {})
-
38
default_headers.merge(headers)
-
end
-
-
1
def require_json_payload?
-
false
-
end
-
-
# determines whether the response should be deemed successful or not
-
# we override this for things like requesting permissions from facebook,
-
# where the response comes back with status 200, but the permissions contained
-
# within aren't sufficient to operate the API
-
1
def default_is_success
-
76
->(response) { response.success? }
-
end
-
-
1
def default_success
-
38
->(response) { response }
-
end
-
-
1
def default_failure
-
38
->(response) {
-
Rails.logger.info "Failed #{self.class.name.demodulize} api request. response: #{response} token: #{@token}"
-
response
-
}
-
end
-
-
1
def default_params
-
152
{ client_id: @key, client_secret: @secret, token_name => @token }.delete_if { |k,v| v.nil? }
-
end
-
-
1
def default_headers
-
38
{ 'Content-Type' => 'application/json; charset=utf-8' }
-
end
-
-
1
def token_name
-
38
:token
-
end
-
-
1
def post_content!(event)
-
raise NotImplementedError.new
-
end
-
-
-
1
def default_host
-
raise NotImplementedError.new
-
end
-
end
-
class Clients::Google < Clients::Base
-
-
def fetch_access_token(code, uri)
-
post "token", params: { code: code, redirect_uri: uri, grant_type: :authorization_code }
-
end
-
-
def fetch_user_info
-
get "userinfo", options: { host: :"https://www.googleapis.com/oauth2/v2" }
-
end
-
-
def scope
-
%w(email profile).freeze
-
end
-
-
private
-
-
def default_headers
-
{ 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8' }
-
end
-
-
def token_name
-
:oauth_token
-
end
-
-
def default_host
-
"https://www.googleapis.com/oauth2/v4".freeze
-
end
-
end
-
class Clients::Nextcloud < Clients::Base
-
-
def fetch_access_token(code, uri)
-
post 'index.php/apps/oauth2/api/v1/token', params: { code: code, redirect_uri: uri, grant_type: :authorization_code }
-
end
-
-
def fetch_user_info
-
get 'ocs/v2.php/cloud/user', params: { format: :json }
-
end
-
-
private
-
-
def default_params
-
{ client_id: @key, client_secret: @secret }.delete_if { |k,v| v.nil? }
-
end
-
-
def authorization_headers
-
{ 'Authorization' => "Bearer #{@token}" }
-
end
-
-
def common_headers
-
{ 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8' }
-
end
-
-
def default_headers
-
if @token
-
common_headers.merge(authorization_headers)
-
else
-
common_headers
-
end
-
end
-
-
def default_host
-
ENV['NEXTCLOUD_HOST']
-
end
-
end
-
class Clients::Oauth < Clients::Base
-
-
def fetch_access_token(code, uri)
-
post ENV.fetch('OAUTH_TOKEN_URL'), params: { code: code, redirect_uri: uri, grant_type: :authorization_code }
-
end
-
-
def fetch_user_info
-
get ENV.fetch('OAUTH_PROFILE_URL')
-
end
-
-
private
-
def perform(method, url, params, headers, options)
-
options.reverse_merge!(
-
success: default_success,
-
failure: default_failure,
-
is_success: default_is_success
-
)
-
Clients::Request.new(method, url, {
-
options[:params_field] => params_for(params),
-
:"headers" => headers_for(headers)
-
}).tap { |request| request.perform!(options) }
-
end
-
-
-
def default_params
-
{ client_id: @key, client_secret: @secret }.delete_if { |k,v| v.nil? }
-
end
-
-
def authorization_headers
-
{ 'Authorization' => "Bearer #{@token}" }
-
end
-
-
def common_headers
-
{ 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8' }
-
end
-
-
def default_headers
-
if @token
-
common_headers.merge(authorization_headers)
-
else
-
common_headers
-
end
-
end
-
end
-
1
class Clients::Recaptcha < Clients::Base
-
1
def validate(recaptcha)
-
req = post_query "siteverify", params: { response: recaptcha, secret: ENV['RECAPTCHA_SECRET_KEY']}
-
Rails.logger.info "recaptcha response #{req.response}"
-
req.response['success']
-
end
-
-
1
private
-
-
1
def default_host
-
"https://www.google.com/recaptcha/api"
-
end
-
-
1
def default_is_success
-
->(response) { response.success? && JSON.parse(response.body)['success'].present? }
-
end
-
end
-
1
Clients::Request = Struct.new(:method, :url, :params) do
-
1
include HTTParty
-
1
default_options.update(verify: false) if ENV['SSL_VERIFY_FALSE']
-
1
default_options.update(verify_peer: false) if ENV['SSL_VERIFY_PEER_FALSE']
-
1
debug_output $stdout
-
-
1
attr_accessor :callback, :success
-
-
1
def json
-
@json ||= callback.call JSON.parse(response.body)
-
end
-
-
1
def perform!(options = {})
-
38
self.success = options[:is_success].call(response)
-
38
self.callback = options[success ? :success : :failure]
-
end
-
-
1
def response
-
76
@response ||= self.class.send(method, url, params)
-
end
-
end
-
class Clients::Slack < Clients::Base
-
-
def fetch_access_token(code, uri)
-
get "oauth.access", params: { code: code, redirect_uri: uri }
-
end
-
-
def fetch_user_info
-
get "users.profile.get", options: { success: ->(response) { response['profile'] } }
-
end
-
-
def fetch_team_info
-
get "team.info", options: { success: ->(response) { response['team'] } }
-
end
-
-
# We are doing two requests and combining them here
-
def fetch_channels
-
channels = get "conversations.list", params: {types: "public_channel,private_channel", limit: 1000}, options: { success: ->(response) { response['channels'] } }
-
-
json = if channels.success
-
[channels].map(&:json).flatten.reject {|channel| channel['name'].starts_with?("mpdm-") }.sort_by {|channel| channel['name'].downcase }
-
else
-
[]
-
end
-
OpenStruct.new(json: json)
-
end
-
-
def post_content!(event)
-
get "chat.postMessage", params: serialized_event(event)
-
end
-
-
def is_member_of?(channel_id, uid)
-
get "channels.info", params: { channel: channel_id }, options: {
-
success: ->(response) { Array(response['channel']['members']).include?(uid) } }
-
end
-
-
def scope
-
%w(users.profile:read channels:read groups:read team:read chat:write:bot commands)
-
end
-
-
private
-
def serialized_event(event)
-
serializer = [
-
"Slack::#{event.kind.classify}Serializer",
-
"Slack::#{event.eventable.class}Serializer",
-
"Slack::BaseSerializer"
-
].detect { |str| str.constantize rescue nil }.constantize
-
serializer.new(event, root: false).as_json
-
end
-
-
def default_is_success
-
->(response) { response.success? && JSON.parse(response.body)['ok'].present? }
-
end
-
-
def default_host
-
"https://slack.com/api".freeze
-
end
-
end
-
1
class Clients::Webhook < Clients::Base
-
-
1
def post_content!(event, format, webhook)
-
post @token, params: serialized_event(event, format, webhook)
-
end
-
-
1
def default_host
-
nil
-
end
-
-
1
def require_json_payload?
-
38
true
-
end
-
-
1
def serialized_event(event, format, webhook)
-
serializer = [
-
"Webhook::#{format.classify}::#{event.kind.classify}Serializer",
-
"Webhook::#{format.classify}::#{event.eventable.class}Serializer",
-
"Webhook::#{format.classify}::BaseSerializer"
-
].detect { |str| str.constantize rescue nil }.constantize
-
serializer.new(event, root: false, scope: {webhook: webhook}).as_json
-
end
-
end
-
1
class GroupExporter
-
1
attr_accessor :group
-
-
EXPORT_MODELS = {
-
1
groups: %w[id key name description created_at],
-
memberships: %w[group_id user_id user_name user_email admin created_at accepted_at],
-
discussions: %w[id group_id author_id author_name title description created_at],
-
comments: %w[id group_id discussion_id author_id author_name title author_name body created_at],
-
polls: %w[id key discussion_id group_id author_id author_name title details closing_at closed_at created_at poll_type custom_fields],
-
stances: %w[id poll_id participant_id author_name reason latest created_at updated_at],
-
outcomes: %w[id poll_id author_id statement created_at updated_at]
-
}.freeze
-
-
1
EXPORT_MODELS.keys.each do |model|
-
7
define_method model, -> {
-
instance_variable_get(:"@#{model}") ||
-
instance_variable_set(:"@#{model}", models_for(model))
-
}
-
-
7
define_method :"#{model.to_s.singularize}_fields", -> { EXPORT_MODELS[model] }
-
end
-
1
attr_reader :field_names
-
-
1
def initialize(group)
-
1
@group = group
-
1
@field_names = {}
-
end
-
-
1
def to_csv(opts = {})
-
CSV.generate(**opts) do |csv|
-
csv << ["Export for #{@group.full_name}"]
-
csv << []
-
-
EXPORT_MODELS.keys.each do |model|
-
csv_append(
-
csv: csv,
-
fields: send(:"#{model.to_s.singularize}_fields"),
-
models: send(:"#{model}"),
-
title: model.to_s.humanize
-
)
-
end
-
end
-
end
-
-
1
private
-
-
1
def models_for(model)
-
model.to_s.classify.constantize.in_organisation(@group).order(created_at: :asc)
-
end
-
-
1
def csv_append(csv:, fields:, models:, title:)
-
csv << ["#{title} (#{models.length})"]
-
csv << fields.map(&:humanize)
-
models.each { |model| csv << fields.map { |field| model.send(field) } }
-
csv << []
-
end
-
end
-
1
ModelLocator = Struct.new(:model, :params) do
-
-
1
def locate
-
490
return nil unless defined?(resource_class)
-
-
490
if model.to_sym == :user
-
5
resource_class.verified.find_by(username: params[:id] || params[:username]) || resource_class.friendly.find(params[:id] || params[:user_id])
-
485
elsif model.to_sym == :group
-
119
(id_param && resource_class.find_by(id: id_param)) ||
-
27
(key_param && resource_class.find_by(key: key_param)) ||
-
resource_class.where.not(handle: nil).find_by(handle: params[:id])
-
366
elsif resource_class.respond_to?(:friendly)
-
216
resource_class.friendly.find key_or_id
-
else
-
150
resource_class.find key_or_id
-
end
-
end
-
-
1
def locate!
-
466
locate or raise ActiveRecord::RecordNotFound
-
end
-
-
1
private
-
-
1
def id_param
-
211
key_or_id.to_i.to_s == key_or_id && key_or_id
-
end
-
-
1
def key_param
-
54
key_or_id.to_i.to_s != key_or_id && key_or_id
-
end
-
-
1
def key_or_id
-
1134
(params[:"#{model}_id"] || params[:"#{model}_key"] || params[:key] || params[:id]).to_s
-
end
-
-
1
def resource_class
-
863
@resource_class ||= model.to_s.camelize.constantize
-
end
-
end
-
1
class PollExporter
-
1
include Routing
-
-
1
def initialize(poll)
-
2
@poll = poll
-
end
-
-
1
def file_name
-
1
"poll-#{@poll.id}-#{@poll.key}-#{@poll.title.parameterize}.csv"
-
end
-
-
1
def meta_table
-
2
outcome = @poll.current_outcome
-
-
{
-
2
id: @poll.id,
-
group_id: @poll.group_id,
-
discussion_id: @poll.discussion_id,
-
author_id: @poll.author.id,
-
title: @poll.title,
-
author_name: @poll.author.name,
-
created_at: @poll.created_at,
-
closed_at: @poll.closed_at,
-
decided_voters_count: @poll.decided_voters_count,
-
undecided_voters_count: @poll.undecided_voters_count,
-
voters_count: @poll.voters_count,
-
details: @poll.details,
-
group_name: @poll.group&.full_name,
-
discussion_title: @poll.discussion&.title,
-
outcome_author_id: outcome&.author_id,
-
outcome_author_name: outcome&.author&.name,
-
outcome_created_at: outcome&.created_at,
-
outcome_statement: outcome&.statement,
-
poll_url: poll_url(@poll)
-
}.compact
-
end
-
-
1
def to_csv(opts={})
-
1
CSV.generate do |csv|
-
1
csv << ['poll']
-
1
csv << meta_table.keys
-
1
csv << meta_table.values
-
1
csv << ['poll_options']
-
1
results = PollService.calculate_results(@poll, @poll.poll_options)
-
1
keys = %w[id poll_id name name_format rank score score_percent max_score_percent voter_percent average voter_count color]
-
1
csv << keys
-
3
results.each { |r| csv << r.slice(*keys).values }
-
1
csv << ['votes']
-
1
csv << ['id', 'poll_id', 'voter_id', 'voter_name', 'created_at', 'updated_at', 'reason', 'reason_format'] + @poll.poll_option_names
-
1
@poll.stances.latest.each do |stance|
-
line = [
-
3
stance.id,
-
stance.poll_id,
-
stance.participant_id,
-
stance.author_name,
-
stance.created_at&.iso8601,
-
stance.updated_at&.iso8601,
-
stance.reason,
-
stance.reason_format]
-
-
3
@poll.poll_options.each do |poll_option|
-
3
line.push(stance.option_scores[poll_option.id.to_s] || nil)
-
end
-
3
csv << line
-
end
-
end
-
end
-
end
-
class Queries::AdminGroupPage
-
def self.members_per_day_sql(group)
-
"select date_trunc('day', created_at) date, count(distinct user_id) from memberships where group_id = #{group.id} group by date order by date"
-
end
-
-
def self.threads_per_day_sql(group)
-
"select date_trunc('day', created_at) date, count(id) from discussions where group_id IN (#{group.id_and_subgroup_ids.join(',')}) group by date order by date"
-
end
-
-
def self.polls_per_day_sql(group)
-
"select date_trunc('day', created_at) date, count(id) from polls where group_id IN (#{group.id_and_subgroup_ids.join(',')}) group by date order by date"
-
end
-
-
def self.thread_events_per_day(group)
-
ids = Discussion.where(group_id: group.id_and_subgroup_ids).pluck(:id)
-
if ids.any?
-
"select date_trunc('day', created_at) date, count(id) from events where discussion_id IN (#{ids.join(',')}) group by date order by date"
-
else
-
"select date_trunc('day', created_at) date, count(id) from events where discussion_id = 0 group by date order by date"
-
end
-
end
-
-
def self.execute(sql)
-
ActiveRecord::Base.connection.execute(sql)
-
end
-
-
def self.run_per_day(sql)
-
massage execute(sql)
-
end
-
-
def self.massage(records)
-
records.to_a.map { |record| { date: record["date"], count: record["count"] } }
-
end
-
-
def self.fetch_data(group)
-
{
-
events: run_per_day(thread_events_per_day(group)),
-
members: run_per_day(members_per_day_sql(group)),
-
threads: run_per_day(threads_per_day_sql(group)),
-
polls: run_per_day(polls_per_day_sql(group))
-
}
-
end
-
-
def self.thread_items_count(group)
-
Discussion.where(group_id: group.id_and_subgroup_ids).sum(:items_count)
-
end
-
-
def self.discussions_count(group)
-
Discussion.where(group_id: group.id_and_subgroup_ids).count
-
end
-
-
def self.polls_count(group)
-
Poll.where(group_id: group.id_and_subgroup_ids).count
-
end
-
-
end
-
1
class Queries::ExploreGroups < Delegator
-
1
def initialize
-
11
min_members = ENV.fetch('EXPLORE_MIN_MEMBERS', 0)
-
11
min_threads = ENV.fetch('EXPLORE_MIN_THREADS', 0)
-
11
require_subscription = ENV.fetch('EXPLORE_REQUIRE_SUBSCRIPTION', false)
-
-
11
@relation = Group.where(listed_in_explore: true)
-
.parents_only
-
.published
-
.where('groups.name IS NOT NULL')
-
.where('groups.memberships_count > ?', min_members)
-
.where('groups.discussions_count > ?', min_threads)
-
-
11
if require_subscription
-
@relation = @relation.eager_load(:subscription)
-
.where("subscriptions.state = 'active'")
-
end
-
-
11
@relation
-
end
-
-
1
def search_for(q)
-
6
@relation = @relation.explore_search(q) if q.present?
-
6
self
-
end
-
-
1
def __getobj__
-
42
@relation
-
end
-
end
-
class Queries::GroupStats
-
def self.comments_count(group_ids)
-
Discussion.where(group_id: group_ids).sum { |d| d.comments.count }
-
end
-
-
def self.discussions_count(group_ids)
-
Discussion.where(group_id: group_ids).count
-
end
-
-
def self.polls_count(group_ids)
-
Poll.where(group_id: group_ids).count
-
end
-
-
def self.voters_count(group_ids)
-
Poll.where(group_id: group_ids).sum(:voters_count)
-
end
-
-
def self.poll_types_count(group_ids)
-
Poll.where(group_id: group_ids).group(:poll_type).count
-
end
-
end
-
1
class Queries::UnionQuery
-
1
def self.for(table_name, queries)
-
8
from = Array(queries).map(&:to_sql).map(&:presence).compact.join(" UNION ")
-
8
table_name.to_s.singularize.camelize.constantize.distinct.from("(#{from}) as #{table_name}")
-
end
-
end
-
1
class Queries::UsersByVolumeQuery
-
1
def self.normal_or_loud(model)
-
680
users_by_volume(model, '>=', DiscussionReader.volumes[:normal])
-
end
-
-
1
def self.email_notifications(model)
-
673
normal_or_loud(model)
-
end
-
-
1
def self.app_notifications(model)
-
740
users_by_volume(model, '>=', DiscussionReader.volumes[:quiet])
-
end
-
-
1
%w(mute quiet normal loud).map(&:to_sym).each do |volume|
-
4
define_singleton_method volume, ->(model) {
-
257
users_by_volume(model, '=', DiscussionReader.volumes[volume])
-
}
-
end
-
-
1
private
-
-
1
def self.users_by_volume(model, operator, volume)
-
1677
return User.none if model.nil?
-
1676
User.active.distinct.
-
joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{model.discussion_id || 0} AND dr.user_id = users.id").
-
joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{model.group_id || 0}").
-
joins("LEFT OUTER JOIN stances s ON s.participant_id = users.id AND s.poll_id = #{model.poll_id || 0} AND s.latest = TRUE").
-
where('(m.id IS NOT NULL AND m.revoked_at IS NULL) OR
-
(dr.id IS NOT NULL AND dr.guest = TRUE AND dr.revoked_at IS NULL) OR
-
(s.id IS NOT NULL AND s.guest = TRUE AND s.revoked_at IS NULL) OR
-
(m.id IS NULL and dr.id IS NULL and s.id IS NULL)').
-
where("coalesce(s.volume, dr.volume, m.volume, 2) #{operator} :volume", volume: volume)
-
end
-
end
-
1
class RangeSet
-
1
def self.includes?(haystack, needle)
-
437
to_ranges(needle).all? do |a|
-
496
to_ranges(haystack).any? { |b| range_includes?(b, a) }
-
end
-
end
-
-
1
def self.range_includes?(a, b)
-
66
return false if (a.length == 0 || b.length == 0)
-
66
a[0] <= b[0] && a[1] >= b[1]
-
end
-
-
1
def self.length(ranges)
-
13
ranges.map {|range| range[1] - range[0] + 1}.sum
-
end
-
-
# do 2 ranges overlap?
-
1
def self.overlaps?(a,b)
-
27
sorted = [a,b].sort_by{|r| r[0] }
-
9
sorted[0][1] >= sorted[1][0]
-
end
-
-
1
def self.to_ranges(ranges)
-
# ranges is supposed to be an array of ranges.
-
# but it's useful to support
-
# range set
-
1297
return [] if ranges.nil?
-
# single id
-
1297
return [[ranges, ranges]] if ranges.is_a? Numeric
-
# single range
-
457
return [[ranges.first, ranges.last]] if ranges.is_a? Range
-
# array of ids
-
476
return ranges.map {|id| [id,id] } if ranges.is_a?(Array) && ranges.first.is_a?(Numeric)
-
# serialized array of range pairs
-
448
return parse(ranges) if ranges.is_a? String
-
# as well as a well formatted array of ranges
-
445
ranges
-
end
-
-
1
def self.intersect_ranges(ranges1, ranges2)
-
41
set1 = ranges_to_list(ranges1)
-
41
set2 = ranges_to_list(ranges2)
-
81
ranges_from_list(set1.select { |id| set2.include? id })
-
end
-
-
1
def self.ranges_to_list(ranges)
-
164
ranges.map {|range| (range[0]..range[1]).to_a}.flatten
-
end
-
-
1
def self.subtract_range(whole, part)
-
# examples
-
# read nothing
-
378
return [whole] if part.empty? || (part.first > whole.last) || (part.last < whole.first)
-
-
# read the whole thing
-
# range [2,3]
-
# read_range [2,3] or [1,4]
-
# unread_ranges []
-
51
return [] if (part.first <= whole.first) && (part.last >= whole.last)
-
-
# read the middle
-
# range [1,3]
-
# read_range [2,2]
-
# unread_ranges [[1,1],[3,3]]
-
49
return [[whole.first, part.first - 1], [part.last + 1, whole.last]] if (part.first > whole.first) && (part.last < whole.last)
-
-
# read the first part
-
# range [1,3]
-
# read_range [1,2]
-
# unread_ranges [[3,3]]
-
11
return [[part.last + 1, whole.last]] if (part.first == whole.first) && (part.last < whole.last)
-
-
# read the last part
-
# range [1,3]
-
# read_range [2,3]
-
# unread_ranges [[1,1]]
-
# start of unread_range is either same as range.first or read_range.last
-
5
return [[whole.first, part.first - 1]] if (part.first > whole.first) && (part.last == whole.last)
-
end
-
-
# all ranges: [[1,2]] , some ranges: [[1,1]]
-
-
1
def self.subtract_ranges(wholes, parts)
-
8
wholes = reduce(wholes)
-
8
parts = reduce(parts)
-
8
parts.each do |part|
-
48
output = []
-
48
wholes.each do |whole|
-
372
output = reduce output.concat(subtract_range(whole, part))
-
end
-
48
wholes = output
-
end
-
8
wholes
-
end
-
-
# for turning an array of likely to be sequential ids into ranges (eg: pluck -> ranges)
-
1
def self.ranges_from_list(ids)
-
1313
return [] if ids.empty?
-
919
ranges = []
-
-
919
last_id = ids.first
-
919
first_id = ids.first
-
-
919
ids.each do |id|
-
1862
if id == last_id + 1
-
934
last_id = id
-
else
-
928
ranges << [first_id,last_id]
-
928
first_id = id
-
928
last_id = id
-
end
-
end
-
919
ranges << [first_id,last_id]
-
919
reduce ranges
-
end
-
-
1
def self.parse(string)
-
# ranges string format [[1,2], [4,5]] == 1-2,4-5
-
2120
string.to_s.split(',').map do |pair|
-
781
pair.split('-').map(&:to_i)
-
end
-
end
-
-
1
def self.serialize(ranges)
-
3107
ranges.map{|r| [r.first,r.last].join('-')}.join(',')
-
end
-
-
1
def self.reduce(ranges)
-
# or ranges[0][0].nil? is a regert: https://bugs.loomio.io/organizations/loomio/issues/499/
-
3012
return [] if ranges.length == 0 or ranges[0][0].nil?
-
8515
ranges = ranges.sort_by {|r| r.first }
-
2637
reduced = [ranges.shift]
-
2637
ranges.each do |r|
-
3241
lastr = reduced[-1]
-
3241
if lastr.last >= r.first - 1
-
1011
reduced[-1] = [lastr.first,[r.last, lastr.last].max]
-
else
-
2230
reduced.push(r)
-
end
-
end
-
2637
reduced
-
end
-
end
-
1
class TimeZoneToCity
-
1
def self.convert(iana_name)
-
207
city_guess = iana_name.split('/')[1]
-
207
if city_list.include? city_guess
-
1
city_guess
-
else
-
206
offset = offset_for_iana(iana_name)
-
206
city_from_offset(offset)
-
end
-
end
-
-
1
def self.city_list
-
413
ActiveSupport::TimeZone::MAPPING.keys
-
end
-
-
1
def self.city_from_offset(offset)
-
206
offsets_with_city[offset]
-
end
-
-
1
def self.offset_for_iana(iana_name)
-
206
ActiveSupport::TimeZone[iana_name].try(:formatted_offset) || "+00:00"
-
end
-
-
1
def self.offsets_with_city
-
31312
a = city_list.map { |city| [ActiveSupport::TimeZone[city].formatted_offset, city] }.flatten
-
206
Hash[*a]
-
end
-
end
-
1
class UserInviter
-
1
def self.count(emails: , user_ids:, chatbot_ids:, audience:, model:, usernames: , actor:, exclude_members: false, include_actor: false)
-
2
emails = Array(emails).map(&:presence).compact.uniq
-
2
user_ids = Array(user_ids).uniq.compact.map(&:to_i)
-
2
chatbot_ids = Array(chatbot_ids).uniq.compact.map(&:to_i)
-
2
usernames = Array(usernames).map(&:presence).compact.uniq
-
-
2
audience_ids = AnnouncementService.audience_users(
-
model, audience, actor, exclude_members, include_actor).pluck(:id)
-
2
email_count = emails.count - User.where(email: emails).count
-
2
users = User.active.where('email in (:emails) or id in (:user_ids) or username IN (:usernames)',
-
emails: emails,
-
usernames: usernames,
-
user_ids: user_ids.concat(audience_ids))
-
2
users = users.where.not(id: model.voter_ids) if exclude_members
-
2
email_count + users.count + chatbot_ids.length
-
end
-
-
1
def self.authorize_add_members!(parent_group:, group_ids:, emails:, user_ids:, actor: )
-
17
subscription = Subscription.for(parent_group)
-
-
17
raise Subscription::NotActive unless subscription.is_active?
-
-
# authorize ability to add members to selected groups
-
17
Group.where(id: group_ids).each do |g|
-
18
actor.ability.authorize!(:add_members, g)
-
end
-
-
16
return if subscription.max_members.nil?
-
-
3
new_count = new_members_count(parent_group: parent_group, user_ids: user_ids, emails: emails)
-
-
3
if (parent_group.org_members_count + new_count) > parent_group.subscription.max_members.to_i
-
3
raise Subscription::MaxMembersExceeded
-
end
-
end
-
-
# how many totally new members are being added right now?
-
1
def self.new_members_count(parent_group:, user_ids:, emails:)
-
new_emails_count =
-
3
emails.uniq.count -
-
Membership.active.where(
-
group_id: parent_group.id_and_subgroup_ids,
-
user_id: User.where(email: emails).pluck(:id),
-
).count
-
-
new_user_ids_count =
-
3
user_ids.uniq.count -
-
Membership.active.where(
-
group_id: parent_group.id_and_subgroup_ids,
-
user_id: user_ids,
-
).count
-
-
3
new_emails_count + new_user_ids_count
-
end
-
-
1
def self.authorize!(emails: , user_ids:, audience:, model:, actor:)
-
# check inviter can notify group if that's happening
-
# check inviter can invite guests (from the org, or external) if that's happening
-
490
user_ids = Array(user_ids).uniq.compact.map(&:to_i)
-
490
emails = Array(emails).map(&:presence).compact.uniq
-
-
# members belong to group
-
490
member_ids = model.members.where(id: user_ids).pluck(:id)
-
490
member_ids += model.members.where(email: emails).pluck(:id)
-
-
490
emails -= User.where(email: emails, id: member_ids).pluck(:email)
-
-
# guests are outside of the group, but allowed to be referenced by user query
-
490
guest_ids = UserQuery.invitable_user_ids(model: model, actor: actor, user_ids: user_ids - member_ids)
-
-
490
actor.ability.authorize!(:announce, model) if audience == 'group'
-
487
actor.ability.authorize!(:add_members, model) if member_ids.any?
-
487
actor.ability.authorize!(:add_guests, model) if emails.any? or guest_ids.any?
-
end
-
-
1
def self.where_existing(user_ids:, audience:, model:, actor:)
-
user_ids = Array(user_ids).uniq.compact.map(&:to_i)
-
audience_ids = AnnouncementService.audience_users(model, audience, actor).pluck(:id)
-
model.members.where('users.id': user_ids + audience_ids)
-
end
-
-
1
def self.where_or_create!(emails:, user_ids:, audience: nil, model:, actor:, include_actor: false)
-
1214
user_ids = Array(user_ids).uniq.compact.map(&:to_i)
-
1214
emails = Array(emails).uniq.compact
-
-
1214
audience_ids = if audience
-
180
AnnouncementService.audience_users(model, audience, actor, false, include_actor).pluck(:id)
-
else
-
1034
[]
-
end
-
-
# guests are any user outside of the group, and not yet invited
-
# either by email address or by user_id, but user_ids are limited to your org
-
1214
member_ids = model.members.where(id: user_ids).pluck(:id)
-
-
# guests are outside of the group, but allowed to be referenced by user query
-
1214
guest_ids = UserQuery.invitable_user_ids(model: model, actor: actor, user_ids: user_ids - member_ids)
-
-
1214
ids = member_ids.concat(guest_ids).concat(audience_ids).uniq
-
-
1214
ThrottleService.limit!(key: 'UserInviterInvitations',
-
id: actor.id,
-
max: actor.invitations_rate_limit,
-
inc: emails.length + ids.length,
-
per: :day)
-
-
1214
wday = Date.today.wday
-
1214
User.import(safe_emails(emails).map do |email|
-
38
User.new(email: email,
-
time_zone: actor.time_zone,
-
date_time_pref: actor.date_time_pref,
-
detected_locale: actor.locale,
-
email_catch_up_day: wday)
-
end, on_duplicate_key_ignore: true)
-
-
1214
User.active.where("id in (:ids) or email in (:emails)", ids: ids, emails: emails)
-
end
-
-
1
private
-
-
1
def self.safe_emails(emails)
-
1252
emails.uniq.reject {|email| NoSpam::SPAM_REGEX.match?(email) }
-
end
-
end
-
1
class UsernameGenerator
-
1
attr_reader :user
-
-
1
def initialize(user)
-
6694
@user = user
-
end
-
-
1
def generate
-
6694
return safe_username unless conflict_exists?(safe_username)
-
-
110
low = 1
-
110
high = 1
-
110
begin
-
192
username = "#{safe_username}#{(low..high).to_a.sample}"
-
192
high = high * 2
-
end while conflict_exists?(username)
-
110
username
-
end
-
-
1
private
-
-
1
def base_username
-
13470
if user.name.present?
-
13366
user.name
-
104
elsif user.email.present?
-
104
user.email.split('@').first
-
else
-
'unknownuser'
-
end
-
end
-
-
1
def safe_username
-
13470
ActiveSupport::Inflector.transliterate(base_username)
-
.downcase
-
.gsub(/[^a-z0-9]+/, '')[0,18]
-
end
-
-
1
def conflict_exists?(username)
-
6886
User.where(username: username).exists?
-
end
-
end
-
module ApplicationHelper
-
def vue_css_includes
-
vue_index = File.read(Rails.root.join('public/blient/index.html'))
-
Nokogiri::HTML(vue_index).css('head link[as=style], head link[rel=stylesheet]').to_s
-
end
-
-
def vue_js_includes
-
vue_index = File.read(Rails.root.join('public/blient/index.html'))
-
Nokogiri::HTML(vue_index).css('head link[as=script], script').to_s
-
end
-
-
def metadata
-
@metadata ||= if should_have_metadata && current_user.can?(:show, resource)
-
"Metadata::#{controller_name.singularize.camelize}Serializer".constantize.new(resource)
-
else
-
{title: AppConfig.theme[:site_name], image_urls: []}
-
end.as_json
-
end
-
-
def resource
-
ModelLocator.new(resource_name, params).locate
-
end
-
-
def assign_resource
-
instance_variable_get("@#{resource_name}") ||
-
instance_variable_set("@#{resource_name}", resource)
-
end
-
-
def resource_name
-
controller_name.singularize
-
end
-
-
def should_have_metadata
-
%w{discussion group poll user}.include? controller_name.singularize.downcase
-
end
-
end
-
module CurrentUserHelper
-
include PendingActionsHelper
-
-
class SpamUserDeniedError < StandardError
-
end
-
-
def sign_in(user)
-
@current_user = nil
-
user = UserService.verify(user: user)
-
super(user) && handle_pending_actions(user) && associate_user_to_visit
-
end
-
-
def current_user
-
@current_user || super || LoggedOutUser.new(locale: logged_out_preferred_locale, params: params, session: session)
-
end
-
-
def deny_spam_users
-
if NoSpam::SPAM_REGEX.match?(current_user.email)
-
raise SpamUserDeniedError.new(current_user.email)
-
end
-
end
-
-
def require_current_user
-
respond_with_error(status: 401) unless current_user && current_user.is_logged_in?
-
end
-
-
private
-
-
def restricted_user
-
User.find_by!(params.slice(:unsubscribe_token).permit!).tap { |user| user.restricted = true } if params[:unsubscribe_token]
-
end
-
-
def set_last_seen_at
-
current_user.update_attribute :last_seen_at, Time.now
-
end
-
end
-
module Dev::DashboardHelper
-
-
private
-
-
def pinned_discussion
-
create_discussion!(:pinned_discussion) { |discussion| pin!(discussion) }
-
end
-
-
def poll_discussion
-
create_discussion!(:poll_discussion, group: create_poll_group) { |discussion| add_poll!(discussion) }
-
end
-
-
def recent_discussion(group: create_group)
-
create_discussion!(:recent_discussion, group: group)
-
end
-
-
def old_discussion
-
create_discussion!(:old_discussion) { |discussion| discussion.update last_activity_at: 2.years.ago }
-
end
-
-
def create_discussion!(name, group: create_group, author: patrick)
-
var_name = :"@#{name}"
-
if existing = instance_variable_get(var_name)
-
existing
-
else
-
instance_variable_set(var_name, Discussion.create!(title: name.to_s.humanize, group: group, author: author, private: false).tap do |discussion|
-
DiscussionService.create(discussion: discussion, actor: discussion.author)
-
yield discussion if block_given?
-
end)
-
end
-
end
-
-
def pin!(discussion)
-
DiscussionService.pin(discussion: discussion, actor: discussion.author)
-
end
-
-
def add_poll!(discussion, name: 'Test poll', actor: jennifer)
-
PollService.create(poll: Poll.new(poll_type: :poll, poll_option_names: ["Apple", "Banana"], title: name, closing_at: 3.days.from_now, discussion: discussion), actor: actor)
-
end
-
-
end
-
module Dev::FakeDataHelper
-
private
-
-
def saved(obj)
-
obj.tap(&:save!)
-
end
-
-
# only return new'd objects
-
def fake_user(args = {})
-
u = User.new({
-
name: [Faker::Name.name,
-
Faker::TvShows::RuPaul.queen,
-
Faker::Superhero.name,
-
Faker::TvShows::BojackHorseman.character,
-
Faker::Movies::BackToTheFuture.character].sample.truncate(100),
-
email: Faker::Internet.email,
-
password: 'loginlogin',
-
detected_locale: 'en',
-
email_verified: true,
-
date_time_pref: 'day_abbr',
-
legal_accepted: true,
-
experiences: {changePicture: true}
-
}.merge(args))
-
# # u.attach io: open(Faker::Avatar.image)
-
# u.uploaded_avatar.attach io: File.new("#{Rails.root}/spec/fixtures/images/patrick.png"), filename: 'patrick.jpg'
-
# u.update(avatar_kind: :uploaded)
-
u
-
-
end
-
-
def fake_unverified_user(args = {})
-
User.new({
-
email: Faker::Internet.email,
-
email_verified: false,
-
}.merge(args))
-
end
-
-
def fake_group(args = {})
-
defaults = {
-
name: Faker::Company.name,
-
description: [
-
Faker::TvShows::BojackHorseman.quote,
-
Faker::Movies::BackToTheFuture.quote].sample
-
}
-
-
values = defaults.merge(args)
-
values[:handle] = values[:name].parameterize
-
group = Group.new(values)
-
# group.tags = [fake_tag]
-
-
# puts 'attaching'
-
# group.logo.attach(
-
# io: URI.open(Rails.root.join('public/brand/icon_sky_300h.png')),
-
# filename: 'loomiologo.png',
-
# identify: false,
-
# content_type: 'image/png'
-
# )
-
# puts 'attached'
-
# group.cover_photo.attach(io: URI.open(Rails.root.join('public/brand/logo_sky_256h.png')), filename: 'loomiocover.png')
-
-
group
-
end
-
-
def fake_tag(args = {})
-
defaults = {
-
name: Faker::Space.planet,
-
color: Faker::Color.hex_color
-
}
-
Tag.new(defaults.merge(args))
-
end
-
-
def fake_discussion(args = {})
-
Discussion.new({
-
title: [Faker::TvShows::BojackHorseman.tongue_twister,
-
Faker::TvShows::Friends.quote,
-
Faker::Quote.yoda,
-
Faker::Quote.robin].sample.truncate(150),
-
description: [Faker::TvShows::BojackHorseman.quote,
-
Faker::TvShows::Simpsons.quote,
-
Faker::Quote.famous_last_words].sample,
-
private: true,
-
tags: ['spicy'],
-
group: fake_group,
-
author: fake_user}.merge(args))
-
end
-
-
-
def fake_new_comment_event(comment = fake_comment)
-
Events::NewComment.new(
-
user: comment.author,
-
kind: 'new_comment',
-
eventable: comment,
-
discussion: comment.discussion
-
)
-
end
-
-
def fake_new_discussion_event(discussion = fake_discussion)
-
Events::NewDiscussion.new(
-
user: discussion.author,
-
kind: 'new_discussion',
-
eventable: discussion
-
)
-
end
-
-
def fake_poll_created_event(poll = fake_poll)
-
Events::PollCreated.new(
-
user: poll.author,
-
kind: 'poll_created',
-
eventable: poll,
-
discussion: poll.discussion
-
)
-
end
-
-
def fake_stance_created_event(stance = fake_stance)
-
Events::StanceCreated.new(
-
user_id: stance[:participant_id],
-
kind: 'stance_created',
-
eventable: stance,
-
discussion: stance.poll.discussion
-
)
-
end
-
-
def fake_outcome_created_event(outcome = fake_outcome)
-
Events::OutcomeCreated.new(
-
user_id: outcome.author_id,
-
kind: 'outcome_created',
-
eventable: outcome,
-
discussion: outcome.discussion
-
)
-
end
-
-
def fake_membership(args = {})
-
Membership.new({
-
group: fake_group,
-
user: fake_user,
-
}.merge(args))
-
end
-
-
def fake_membership_request(args = {})
-
MembershipRequest.new({
-
requestor: fake_user,
-
group: fake_group
-
}.merge(args))
-
end
-
-
def fake_identity(args = {})
-
Identities::Base.new({
-
user: fake_user,
-
uid: "abc",
-
access_token: SecureRandom.uuid,
-
identity_type: :slack
-
}.merge(args))
-
end
-
-
def option_names(option_count)
-
seed = (0..20).to_a.sample
-
options = option_count.times.map do
-
[
-
Faker::Food.ingredient,
-
Faker::Movies::StarWars.call_squadron
-
].sample.truncate(250)
-
end.uniq
-
{
-
poll: options,
-
proposal: %w[agree abstain disagree block],
-
count: %w[accept decline],
-
check: %w[looks_good not_sure concerned],
-
dot_vote: options,
-
meeting: option_count.times.map { |i| (seed+i).days.from_now.iso8601},
-
ranked_choice: options,
-
score: options
-
}.with_indifferent_access
-
end
-
-
def fake_poll(args = {})
-
names = option_names(args.delete(:option_count) || (2..7).to_a.sample)
-
-
closing_at = args[:wip] ? nil : 3.days.from_now
-
options = {
-
author: fake_user,
-
discussion: fake_discussion,
-
poll_type: 'poll',
-
title: [Faker::Superhero.name, Faker::Movies::StarWars.quote].sample.truncate(140),
-
tags: ['biggin'],
-
details: [
-
Faker::Movies::StarWars.quote,
-
Faker::Movies::HitchhikersGuideToTheGalaxy.marvin_quote,
-
Faker::Movies::PrincessBride.quote,
-
Faker::Movies::Lebowski.quote,
-
Faker::Movies::HitchhikersGuideToTheGalaxy.quote].sample,
-
poll_option_names: names[args.fetch(:poll_type, :poll)],
-
closing_at: closing_at,
-
specified_voters_only: false,
-
custom_fields: {}
-
}.merge args.tap {|a| a.delete(:wip)}
-
-
case options[:poll_type].to_s
-
when 'dot_vote'
-
options[:dots_per_person] = 10
-
when 'meeting'
-
options[:time_zone] = 'Asia/Seoul'
-
options[:can_respond_maybe] = true
-
when 'ranked_choice'
-
options[:minimum_stance_choices] = 3
-
when 'score'
-
options[:max_score] = 9
-
options[:min_score] = -9
-
end
-
-
Poll.new(options)
-
end
-
-
def create_fake_stances(poll:)
-
(2..7).to_a.sample.times do
-
u = fake_user
-
poll.group.add_member!(u) if poll.group
-
stance = fake_stance(poll: poll)
-
stance.save!
-
stance.create_missing_created_event!
-
end
-
poll.update_counts!
-
end
-
-
-
-
def fake_score(poll, index = 0)
-
case poll.poll_type
-
when 'score'
-
((poll.min_score)..(poll.max_score)).to_a.sample
-
when 'ranked_choice'
-
index + 1
-
when 'meeting'
-
if poll.can_respond_maybe
-
[0,1,2].sample
-
else
-
[0,2].sample
-
end
-
else
-
1
-
end
-
end
-
-
def cast_stance_params(poll)
-
if poll.require_all_choices
-
num_choices = poll.poll_options.length
-
else
-
num_choices = (poll.minimum_stance_choices..poll.maximum_stance_choices).to_a.sample
-
end
-
-
choice = poll.poll_options.sample(num_choices).map.with_index do |option, index|
-
score = fake_score(poll)
-
[option.name, fake_score(poll, index)]
-
end.to_h
-
-
reason = [
-
Faker::Hipster.sentence,
-
Faker::GreekPhilosophers.quote,
-
Faker::TvShows::RuPaul.quote,
-
""
-
].sample
-
-
{
-
choice: choice,
-
reason: reason
-
}
-
end
-
-
def fake_stance(args = {})
-
poll = args[:poll] || saved(fake_poll)
-
-
if poll.require_all_choices
-
num_choices = poll.poll_options.length
-
else
-
num_choices = (poll.minimum_stance_choices..poll.maximum_stance_choices).to_a.sample
-
end
-
-
choice = poll.poll_options.sample(num_choices).map.with_index do |option, index|
-
score = fake_score(poll)
-
[option.name, fake_score(poll, index)]
-
end.to_h
-
-
Stance.new({
-
poll: poll,
-
participant: fake_user,
-
reason: [
-
Faker::Hipster.sentence,
-
Faker::GreekPhilosophers.quote,
-
Faker::TvShows::RuPaul.quote,
-
""].sample,
-
choice: choice
-
}.merge(args))
-
end
-
-
def fake_comment(args = {})
-
Comment.new({
-
discussion: fake_discussion,
-
body: Faker::ChuckNorris.fact,
-
author: fake_user
-
}.merge(args))
-
end
-
-
def fake_reaction(args = {})
-
Reaction.new({
-
reactable: fake_comment,
-
user: fake_user,
-
reaction: "+1"
-
}.merge(args))
-
end
-
-
def fake_outcome(args = {})
-
poll = fake_poll
-
Outcome.new({
-
poll: poll,
-
author: poll.author,
-
statement: with_markdown(Faker::Hipster.sentence)
-
}.merge(args))
-
end
-
-
def fake_received_email(args = {})
-
ReceivedEmail.new({
-
sender_email: Faker::Internet.email,
-
subject: Faker::ChuckNorris.fact,
-
body: "FORWARDED MESSAGE------ TO: Mary <mary@example.com>, beth@example.com, Tim <tim@example.com> SUBJECT: We're having an argument! blahblahblah",
-
})
-
end
-
-
def create_group_with_members
-
group = saved(fake_group)
-
group.add_admin!(saved(fake_user))
-
(7..9).to_a.sample.times do
-
group.add_member!(saved(fake_user))
-
end
-
create_chatbots_for_group(group)
-
group
-
end
-
-
def create_chatbots_for_group(group)
-
event_kinds = %w[
-
new_discussion
-
discussion_edited
-
poll_created
-
poll_edited
-
poll_closing_soon
-
poll_expired
-
poll_announced
-
poll_reopened
-
outcome_created
-
]
-
-
if ENV['TEST_MATRIX_SERVER']
-
Chatbot.create!(
-
group: group,
-
kind: "matrix",
-
server: ENV['TEST_MATRIX_SERVER'],
-
channel: ENV['TEST_MATRIX_CHANNEL'],
-
access_token: ENV['TEST_MATRIX_ACCESS_TOKEN'],
-
event_kinds: event_kinds,
-
# notification_only: true,
-
name: "Matrix"
-
)
-
end
-
-
if ENV['TEST_TEAMS_WEBHOOK']
-
Chatbot.create!(
-
group: group,
-
kind: "webhook",
-
webhook_kind: "microsoft",
-
server: ENV['TEST_TEAMS_WEBHOOK'],
-
event_kinds: event_kinds,
-
# notification_only: true,
-
name: "Microsoft Teams"
-
)
-
end
-
-
if ENV['TEST_SLACK_WEBHOOK']
-
Chatbot.create!(
-
group: group,
-
kind: "webhook",
-
webhook_kind: "slack",
-
server: ENV['TEST_SLACK_WEBHOOK'],
-
event_kinds: event_kinds,
-
# notification_only: true,
-
name: "Slack"
-
)
-
end
-
-
if ENV['TEST_DISCORD_WEBHOOK']
-
Chatbot.create!(
-
group: group,
-
kind: "webhook",
-
webhook_kind: "discord",
-
server: ENV['TEST_DISCORD_WEBHOOK'],
-
event_kinds: event_kinds,
-
# notification_only: true,
-
name: "Discord"
-
)
-
end
-
end
-
-
def create_fake_poll_in_group(args = {})
-
saved(build_fake_poll_in_group)
-
end
-
-
def create_discussion_with_nested_comments
-
group = create_group_with_members
-
group.reload
-
discussion = saved fake_discussion(group: group)
-
DiscussionService.create(discussion: discussion, actor: group.admins.first)
-
-
15.times do
-
parent_author = fake_user
-
group.add_member! parent_author
-
parent = fake_comment(discussion: discussion)
-
CommentService.create(comment: parent, actor: parent_author)
-
-
(0..3).to_a.sample.times do
-
reply_author = fake_user
-
group.add_member! reply_author
-
reply = fake_comment(discussion: discussion, parent: parent)
-
CommentService.create(comment: reply, actor: reply_author)
-
end
-
end
-
-
discussion.reload
-
EventService.repair_thread(discussion.id)
-
discussion.reload
-
end
-
-
def create_discussion_with_sampled_comments
-
group = create_group_with_members
-
-
discussion = saved fake_discussion(group: group)
-
DiscussionService.create(discussion: discussion, actor: group.admins.first)
-
discussion.update(max_depth: 3)
-
-
5.times do
-
group.add_member! saved(fake_user)
-
end
-
-
10.times do
-
CommentService.create(comment: fake_comment(discussion: discussion), actor: group.members.sample)
-
end
-
comments = discussion.reload.comments
-
-
10.times do
-
CommentService.create(comment: fake_comment(discussion: discussion, parent: comments.sample), actor: group.members.sample)
-
end
-
-
comments = discussion.reload.comments
-
-
10.times do
-
CommentService.create(comment: fake_comment(discussion: discussion, parent: comments.sample), actor: group.members.sample)
-
end
-
-
discussion.reload
-
EventService.repair_thread(discussion.id)
-
discussion.reload
-
discussion
-
end
-
-
-
private
-
-
def with_markdown(text)
-
"#{text} - **(markdown!)**"
-
end
-
end
-
module Dev::NintiesMoviesHelper
-
include Dev::FakeDataHelper
-
-
private
-
-
# try to just return objects here. Don't knit them together. Leave that for
-
# the development controller action to do if possible
-
def patrick
-
@patrick ||= User.find_by(email: 'patrick_swayze@example.com') ||
-
User.create!(name: 'Patrick Swayze',
-
email: 'patrick_swayze@example.com',
-
is_admin: false,
-
username: 'patrickswayze',
-
password: 'gh0stmovie',
-
experiences: {changePicture: true},
-
detected_locale: 'en',
-
date_time_pref: 'day_abbr',
-
avatar_kind: 'uploaded',
-
email_verified: true)
-
@patrick.uploaded_avatar.attach io: File.new("#{Rails.root}/spec/fixtures/images/patrick.png"), filename: 'patrick.jpg'
-
@patrick.update(avatar_kind: :uploaded)
-
@patrick
-
end
-
-
def patricks_contact
-
if patrick.contacts.empty?
-
patrick.contacts.create(name: 'Keanu Reeves',
-
email: 'keanu@example.com',
-
date_time_pref: 'day_abbr',
-
source: 'gmail')
-
end
-
end
-
-
def jennifer
-
@jennifer ||= User.find_by(email: 'jennifer_grey@example.com') ||
-
User.create!(name: 'Jennifer Grey',
-
email: 'jennifer_grey@example.com',
-
date_time_pref: 'day_abbr',
-
username: 'jennifergrey',
-
experiences: {changePicture: true},
-
email_verified: true)
-
@jennifer.uploaded_avatar.attach io: File.new("#{Rails.root}/spec/fixtures/images/jennifer.png"), filename: 'jen.jpg'
-
@jennifer.update(avatar_kind: :uploaded)
-
-
@jennifer
-
end
-
-
def max
-
@max ||= User.find_by(email: 'max@example.com') ||
-
User.create!(name: 'Max Von Sydow',
-
email: 'max@example.com',
-
password: 'gh0stmovie',
-
username: 'mingthemerciless',
-
date_time_pref: 'day_abbr',
-
email_verified: true)
-
@max
-
end
-
-
def emilio
-
@emilio ||= User.find_by(email: 'emilio@loomio.org') ||
-
User.create!(name: 'Emilio Estevez',
-
email: 'emilio@loomio.org',
-
password: 'gh0stmovie',
-
date_time_pref: 'day_abbr',
-
email_verified: true)
-
end
-
-
def judd
-
@judd ||= User.find_by(email: 'judd@example.com') ||
-
User.create!(name: 'Judd Nelson',
-
email: 'judd@example.com',
-
password: 'gh0stmovie',
-
date_time_pref: 'day_abbr',
-
email_verified: true)
-
end
-
-
def rudd
-
@rudd ||= User.find_by(email: 'rudd@example.com') ||
-
User.create!(name: 'Paul Rudd',
-
email: 'rudd@example.com',
-
password: 'gh0stmovie',
-
date_time_pref: 'day_abbr',
-
email_verified: true)
-
end
-
-
def create_group
-
unless @group
-
@group = Group.new(name: 'Dirty Dancing Shoes',
-
description: 'The best place for dancing shoes. _every_ shoe is **dirty**!',
-
group_privacy: 'closed',
-
handle: 'shoes',
-
discussion_privacy_options: 'public_or_private', creator: patrick)
-
file = open(Rails.root.join('public','brand','icon_sky_150h.png'))
-
@group.logo.attach(io: file, filename: 'logo.png')
-
GroupService.create(group: @group, actor: @group.creator)
-
@group.add_admin! patrick
-
@group.add_member! jennifer
-
@group.add_member! emilio
-
end
-
@group
-
end
-
-
def create_poll_group
-
unless @poll_group
-
@poll_group = Group.new(name: 'Dirty Dancing Shoes',
-
group_privacy: 'closed',
-
discussion_privacy_options: 'public_or_private',
-
creator: patrick)
-
GroupService.create(group: @poll_group, actor: @poll_group.creator)
-
@poll_group.add_admin! patrick
-
@poll_group.add_member! jennifer
-
@poll_group.add_member! emilio
-
end
-
@poll_group
-
end
-
-
def multiple_groups
-
@groups = []
-
10.times do
-
group = Group.new(name: Faker::Name.name,
-
group_privacy: 'closed',
-
discussion_privacy_options: 'public_or_private', creator: patrick)
-
group.add_admin! patrick
-
GroupService.create(group: group, actor: group.creator)
-
@groups << group
-
end
-
@groups
-
end
-
-
def muted_create_group
-
unless @muted_group
-
@muted_group = Group.new(name: 'Muted Point Blank',
-
group_privacy: 'closed',
-
discussion_privacy_options: 'public_or_private', creator: patrick)
-
GroupService.create(group: @muted_group, actor: @muted_group.creator)
-
@muted_group.add_admin! patrick
-
Membership.find_by(group: @muted_group, user: patrick).set_volume! :mute
-
end
-
@muted_group
-
end
-
-
def create_another_group
-
unless @another_group
-
@another_group = Group.new(name: 'Point Break',
-
group_privacy: 'closed',
-
discussion_privacy_options: 'public_or_private',
-
description: 'An FBI agent goes undercover to catch a gang of bank robbers who may be surfers.', creator: patrick)
-
GroupService.create(group: @another_group, actor: @another_group.creator)
-
@another_group.add_admin! patrick
-
@another_group.add_member! max
-
end
-
@another_group
-
end
-
-
def create_discussion
-
unless @discussion
-
@discussion = Discussion.create(title: 'What star sign are you?', private: false, group: create_group, link_previews: [{'title': 'link title', 'url': 'https://www.example.com', 'description': 'a link to a page', 'image': 'https://www.loomio.org/theme/logo.svg', 'hostname':'www.example.com'}], author: jennifer)
-
DiscussionService.create(discussion: @discussion, actor: @discussion.author)
-
end
-
@discussion
-
end
-
-
def create_another_discussion
-
unless @another_discussion
-
@another_discussion = Discussion.create(title: 'Waking Up in Reno',
-
private: false,
-
group: create_group,
-
author: jennifer)
-
DiscussionService.create(discussion: @another_discussion, actor: @another_discussion.author)
-
end
-
@another_discussion
-
end
-
-
def create_closed_discussion
-
unless @closed_discussion
-
@closed_discussion = Discussion.create(title: 'This thread is old and closed',
-
private: false,
-
closed_at: Time.now,
-
group: create_group,
-
author: jennifer)
-
DiscussionService.create(discussion: @closed_discussion, actor: @closed_discussion.author)
-
end
-
@closed_discussion
-
end
-
-
def create_public_discussion
-
unless @another_discussion
-
@another_discussion = Discussion.create!(title: "The name's Johnny Utah!",
-
private: false,
-
group: create_another_group,
-
author: patrick)
-
DiscussionService.create(discussion: @another_discussion, actor: @another_discussion.author)
-
end
-
@another_discussion
-
end
-
-
def private_create_discussion
-
unless @another_discussion
-
@another_discussion = Discussion.create!(title: 'But are you crazy enough?',
-
private: true,
-
group: create_another_group,
-
author: patrick)
-
DiscussionService.create(discussion: @another_discussion, actor: @another_discussion.author)
-
end
-
@another_discussion
-
end
-
-
def create_subgroup
-
unless @subgroup
-
@subgroup = Group.new(name: 'Johnny Utah',
-
parent: create_another_group,
-
discussion_privacy_options: 'public_or_private',
-
group_privacy: 'closed', creator: patrick)
-
GroupService.create(group: @subgroup, actor: @subgroup.creator)
-
discussion = FactoryBot.create :discussion, group: @subgroup, title: "Vaya con dios", private: false
-
# discussion = @subgroup.discussions.create(title: "Vaya con dios", private: false, author: patrick)
-
DiscussionService.create(discussion: discussion, actor: discussion.author)
-
@subgroup.add_admin! patrick
-
end
-
@subgroup
-
end
-
-
def another_create_subgroup
-
unless @another_subgroup
-
@another_subgroup = Group.new(name: 'Bodhi',
-
parent: create_another_group,
-
group_privacy: 'closed',
-
discussion_privacy_options: 'public_or_private',
-
is_visible_to_parent_members: true, creator: patrick)
-
GroupService.create(group: @another_subgroup, actor: @another_subgroup.creator)
-
discussion = FactoryBot.create :discussion, group: @another_subgroup, title: "Vaya con dios 2", private: false
-
DiscussionService.create(discussion: discussion, actor: discussion.author)
-
@another_subgroup.add_admin! patrick
-
end
-
@another_subgroup
-
end
-
-
def pending_invitation
-
@pending_membership ||= Membership.create(user: User.new(email: 'judd@example.com'),
-
group: create_group, inviter: patrick)
-
end
-
-
def create_comment
-
unless @create_comment
-
@create_comment ||= Comment.create!(
-
discussion: create_discussion,
-
author: patrick,
-
body: 'Hello world!'
-
)
-
end
-
@create_comment
-
end
-
-
def create_poll
-
@create_poll ||= Poll.create!(
-
discussion: create_discussion,
-
poll_type: :proposal,
-
poll_option_names: %w(agree abstain disagree block),
-
author: patrick,
-
title: "Let's go to the moon!",
-
closing_at: 10.days.from_now
-
)
-
end
-
-
def create_stance
-
@create_stance ||= Stance.create(
-
poll: create_poll,
-
participant: patrick,
-
choice: :agree,
-
reason: "I have unreasonably high expectations for how this will go!"
-
)
-
end
-
-
def create_outcome
-
@create_outcome ||= Outcome.create!(
-
poll: create_poll.tap { |p| p.update(closed_at: 1.day.ago) },
-
author: patrick,
-
statement: "Okay let's do it!"
-
)
-
end
-
-
def create_all_activity_items
-
# discussion_edited
-
create_discussion
-
create_discussion.update(title: "another discussion title")
-
Events::DiscussionEdited.publish!(discussion: create_discussion, actor: create_discussion.author)
-
-
# discussion_moved
-
Events::DiscussionMoved.publish!(create_discussion, patrick, create_another_group)
-
-
# new_comment
-
Events::NewComment.publish!(create_comment)
-
-
# poll_created
-
Events::PollCreated.publish!(create_poll, patrick)
-
-
# poll_edited
-
create_poll.update(title: "Another poll title")
-
Events::PollEdited.publish!(poll: create_poll, actor: patrick)
-
-
# stance_created
-
Events::StanceCreated.publish!(create_stance)
-
-
# poll_expired
-
Events::PollExpired.publish!(create_poll)
-
-
# poll_closed_by_user
-
Events::PollClosedByUser.publish!(create_poll, patrick)
-
-
# outcome_created
-
Events::OutcomeCreated.publish!(outcome: create_outcome)
-
end
-
-
-
def create_all_notifications
-
#'reaction_created'
-
patrick_comment = Comment.new(discussion: create_discussion, body: 'I\'m rather likeable')
-
reaction = Reaction.new(reactable: patrick_comment, reaction: ":heart:")
-
new_comment_event = CommentService.create(comment: patrick_comment, actor: patrick)
-
reaction_created_event = ReactionService.update(reaction: reaction, params: {reaction: ':slight_smile:'}, actor: jennifer)
-
create_another_group.add_member! jennifer
-
-
#'comment_replied_to'
-
jennifer_comment = Comment.new(discussion: create_discussion,
-
parent: patrick_comment,
-
body: 'hey @patrickswayze you look great in that tuxeido (jen reply to patrick)')
-
CommentService.create(comment: jennifer_comment, actor: jennifer)
-
-
#'user_mentioned'
-
reply_comment = Comment.new(discussion: create_discussion,
-
body: 'I agree with @patrickswayze (jen mention patrick)', parent: jennifer_comment)
-
CommentService.create(comment: reply_comment, actor: jennifer)
-
-
-
[max, emilio, judd].each {|u| patrick_comment.group.add_member! u}
-
ReactionService.update(reaction: Reaction.new(reactable: patrick_comment), params: {reaction: ':slight_smile:'}, actor: jennifer)
-
ReactionService.update(reaction: Reaction.new(reactable: patrick_comment), params: {reaction: ':heart:'}, actor: patrick)
-
ReactionService.update(reaction: Reaction.new(reactable: patrick_comment), params: {reaction: ':laughing:'}, actor: max)
-
ReactionService.update(reaction: Reaction.new(reactable: patrick_comment), params: {reaction: ':cry:'}, actor: emilio)
-
ReactionService.update(reaction: Reaction.new(reactable: patrick_comment), params: {reaction: ':wave:'}, actor: judd)
-
-
#'membership_requested',
-
membership_request = MembershipRequest.new(group: create_group)
-
event = MembershipRequestService.create(membership_request: membership_request, actor: rudd)
-
-
#'membership_request_approved',
-
another_group = Group.new(name: 'Stars of the 90\'s', group_privacy: 'closed')
-
GroupService.create(group: another_group, actor: jennifer)
-
membership_request = MembershipRequest.new(requestor: patrick, group: another_group)
-
event = MembershipRequestService.create(membership_request: membership_request, actor: patrick)
-
approval_event = MembershipRequestService.approve(membership_request: membership_request, actor: jennifer)
-
-
#'user_added_to_group',
-
#notify patrick that he has been added to jens group
-
another_group = Group.new(name: 'Planets of the 80\'s')
-
GroupService.create(group: another_group, actor: jennifer)
-
jennifer.reload
-
GroupService.invite(group: another_group, params: { recipient_user_ids: [patrick.id] }, actor: jennifer)
-
-
#'new_coordinator',
-
#notify patrick that jennifer has made him a coordinator
-
membership = Membership.find_by(user_id: patrick.id, group_id: another_group.id)
-
new_coordinator_event = MembershipService.make_admin(membership: membership, actor: jennifer)
-
-
#'invitation_accepted',
-
#notify patrick that his invitation to emilio has been accepted
-
membership = Membership.create(user: emilio, group: another_group, inviter: patrick)
-
MembershipService.redeem(membership: membership, actor: emilio)
-
-
poll = FactoryBot.create(:poll, discussion: create_discussion, group: create_group, author: jennifer, closing_at: 24.hours.from_now)
-
PollService.invite(
-
poll: poll,
-
params: { recipient_user_ids: [patrick.id] },
-
actor: jennifer
-
)
-
-
#'poll_closing_soon'
-
PollService.publish_closing_soon
-
-
#'outcome_created'
-
poll = FactoryBot.build(:poll, discussion: create_discussion, author: jennifer, closed_at: 1.day.ago, closing_at: 1.day.ago)
-
-
PollService.create(poll: poll, actor: jennifer)
-
outcome = FactoryBot.build(:outcome, poll: poll)
-
OutcomeService.create(
-
outcome: outcome,
-
params: {recipient_user_ids: [patrick.id]},
-
actor: jennifer
-
)
-
-
#'stance_created'
-
# notify patrick that someone has voted on his proposal
-
poll = FactoryBot.build(:poll, closing_at: 4.days.from_now, discussion: create_discussion, voter_can_add_options: true)
-
PollService.create(poll: poll, actor: patrick)
-
end
-
end
-
module Dev::ScenariosHelper
-
include Dev::FakeDataHelper
-
-
def poll_created_scenario(params)
-
group = create_group_with_members
-
-
discussion = fake_discussion(group: group, title: "Some discussion")
-
DiscussionService.create(discussion: discussion, actor: group.members.first)
-
-
actor = group.admins.first
-
user = saved(fake_user(time_zone: "America/New_York"))
-
-
group.add_member! user if !params[:guest]
-
group.add_admin! user if params[:admin]
-
-
poll = fake_poll(group: group,
-
discussion: params[:standalone] ? nil : discussion,
-
poll_type: params[:poll_type],
-
hide_results: (params[:hide_results] || :off),
-
wip: params[:wip],
-
anonymous: !!params[:anonymous])
-
-
event = PollService.create(poll: poll, actor: actor, params: {notify_recipients: true})
-
-
if params[:guest]
-
recipients = {recipient_emails: [user.email], notify_recipients: true}
-
PollService.invite(poll: poll, params: recipients, actor: actor)
-
end
-
-
{
-
discussion: discussion,
-
group: group,
-
observer: user,
-
poll: event.eventable,
-
title: event.eventable.title,
-
actor: actor,
-
}
-
end
-
-
def poll_closed_scenario(params)
-
observer = fake_user.tap(&:save!)
-
group = create_group_with_members
-
group.add_admin!(observer)
-
poll = fake_poll(poll_type: params[:poll_type],
-
anonymous: !!params[:anonymous],
-
hide_results: (params[:hide_results] || :off),
-
group: group,
-
discussion: nil,
-
wip: params[:wip])
-
-
event = PollService.create(poll: poll, actor: observer)
-
Stance.where(poll_id: poll.id, participant_id: observer.id).delete_all
-
-
stance = fake_stance(poll: poll)
-
StanceService.create(stance: stance, actor: observer)
-
-
PollService.close(poll: poll, actor: observer)
-
-
{
-
observer: observer,
-
group: group,
-
actor: observer,
-
title: event.eventable.title,
-
poll: event.eventable
-
}
-
end
-
-
def poll_user_mentioned_scenario(params)
-
scenario = poll_created_scenario(params)
-
voter = saved(fake_user)
-
group_member = saved(fake_user)
-
scenario[:poll].group.add_member!(voter)
-
scenario[:poll].group.add_member!(group_member)
-
-
stance = Stance.find_by(poll: scenario[:poll], participant: voter, latest: true)
-
-
params = cast_stance_params(scenario[:poll])
-
params[:reason] = "<p><span class='mention' data-mention-id='#{group_member.username}'>@#{group_member.name}</span></p>"
-
params[:reason_format] = "html"
-
-
StanceService.update(stance: stance, actor: voter, params: params)
-
-
scenario[:actor] = voter
-
-
scenario.merge(observer: group_member)
-
end
-
-
def poll_stance_created_scenario(params)
-
scenario = poll_created_scenario(params)
-
voter = saved(fake_user)
-
scenario[:poll].group.add_member!(voter)
-
-
Stance.where(poll_id: scenario[:poll].id,
-
participant_id: scenario[:poll].author_id).update(volume: 'loud')
-
-
stance = Stance.find_by(poll: scenario[:poll], participant: voter, latest: true)
-
event = StanceService.update(stance: stance, actor: voter, params: cast_stance_params(scenario[:poll]))
-
scenario[:stance] = event.eventable
-
scenario[:actor] = event.eventable.participant
-
scenario[:real_actor] = voter
-
-
scenario.merge(observer: scenario[:poll].author, voter: voter)
-
end
-
-
def poll_anonymous_scenario(params)
-
scenario = poll_created_scenario(params)
-
voter = saved(fake_user)
-
scenario[:poll].group.add_member!(voter)
-
choices = [{poll_option_id: scenario[:poll].poll_option_ids[0]}]
-
StanceService.create(stance: fake_stance(poll: scenario[:poll], stance_choices_attributes: choices), actor: voter)
-
scenario[:actor] = voter
-
-
scenario.merge(observer: scenario[:poll].author, voter: voter)
-
end
-
-
def poll_closing_soon_scenario(params)
-
discussion = fake_discussion(group: create_group_with_members)
-
non_voter = saved(fake_user)
-
discussion.group.add_member! non_voter
-
actor = discussion.group.admins.first
-
DiscussionService.create(discussion: discussion, actor: actor)
-
poll = fake_poll(
-
author: actor,
-
poll_type: params[:poll_type],
-
anonymous: !!params[:anonymous],
-
hide_results: (params[:hide_results] || :off),
-
discussion: discussion,
-
wip: params[:wip],
-
notify_on_closing_soon: params[:notify_on_closing_soon] || 'voters',
-
created_at: 6.days.ago,
-
closing_at: if params[:wip] then nil else 1.day.from_now end
-
)
-
PollService.create(poll: poll, actor: actor)
-
-
create_fake_stances(poll: poll)
-
-
PollService.invite(poll: poll, params: {recipient_user_ids: [non_voter.id]}, actor: actor)
-
PollService.publish_closing_soon
-
-
{
-
discussion: discussion,
-
group: discussion.group,
-
observer: non_voter,
-
actor: actor,
-
poll: poll,
-
title: poll.title
-
}
-
end
-
-
def poll_reminder_scenario(params)
-
discussion = fake_discussion(group: create_group_with_members)
-
non_voter = saved(fake_user)
-
discussion.group.add_member! non_voter
-
actor = discussion.group.admins.first
-
DiscussionService.create(discussion: discussion, actor: actor)
-
poll = fake_poll(
-
author: actor,
-
poll_type: params[:poll_type],
-
anonymous: !!params[:anonymous],
-
hide_results: (params[:hide_results] || :off),
-
discussion: discussion,
-
wip: params[:wip],
-
notify_on_closing_soon: params[:notify_on_closing_soon] || 'voters',
-
created_at: 6.days.ago,
-
closing_at: if params[:wip] then nil else 1.day.from_now end
-
)
-
-
PollService.create(poll: poll, actor: actor)
-
create_fake_stances(poll:poll)
-
-
# Stance.create(poll: poll, participant: non_voter)
-
PollService.invite(poll: poll, params: {recipient_user_ids: [non_voter.id]}, actor: actor)
-
-
PollService.remind(poll: poll, params: {recipient_user_ids: [non_voter.id]}, actor: actor)
-
-
{
-
discussion: discussion,
-
group: discussion.group,
-
observer: non_voter,
-
actor: actor,
-
poll: poll,
-
title: poll.title
-
}
-
end
-
-
def poll_closing_soon_author_scenario(params)
-
params[:notify_on_closing_soon] = 'author'
-
scenario = poll_closing_soon_scenario(params)
-
scenario.merge(observer: scenario[:poll].author)
-
end
-
-
def poll_closing_soon_with_vote_scenario(params)
-
discussion = fake_discussion(group: create_group_with_members)
-
actor = discussion.group.admins.first
-
poll = fake_poll(
-
author: actor,
-
poll_type: params[:poll_type],
-
anonymous: !!params[:anonymous],
-
hide_results: (params[:hide_results] || :off),
-
notify_on_closing_soon: :voters,
-
discussion: discussion,
-
closing_at: if params[:wip] then nil else 1.day.from_now end
-
)
-
PollService.create(poll: poll, actor: actor)
-
create_fake_stances(poll: poll)
-
-
voter = poll.stances.last.real_participant
-
discussion.add_guest! voter, discussion.author
-
PollService.invite(poll: poll, params: {recipient_user_ids: [voter.id]}, actor: actor)
-
PollService.publish_closing_soon
-
-
{
-
discussion: discussion,
-
group: discussion.group,
-
observer: voter,
-
actor: actor,
-
title: poll.title,
-
poll: poll
-
}
-
end
-
-
def poll_expired_scenario(params)
-
scenario = poll_expired_author_scenario(params)
-
scenario.merge(observer: scenario[:actor])
-
end
-
-
def poll_expired_author_scenario(params)
-
discussion = fake_discussion(group: create_group_with_members)
-
actor = discussion.group.admins.first
-
params[:discussion] = discussion
-
poll = fake_poll(
-
discussion: discussion,
-
poll_type: params[:poll_type],
-
anonymous: !!params[:anonymous],
-
hide_results: (params[:hide_results] || :off)
-
)
-
PollService.create(poll: poll, actor: actor)
-
create_fake_stances(poll: poll)
-
poll.update_attribute(:closing_at, 1.day.ago)
-
poll.discussion.group.add_member! poll.author
-
PollService.expire_lapsed_polls
-
{
-
discussion: discussion,
-
group: discussion.group,
-
actor: actor,
-
observer: poll.author,
-
title: poll.title,
-
poll: poll
-
}
-
end
-
-
def poll_outcome_created_scenario(params)
-
discussion = saved(fake_discussion(group: create_group_with_members))
-
actor = discussion.group.admins.first
-
observer = fake_user
-
discussion.group.add_member! observer
-
poll = fake_poll(
-
poll_type: params[:poll_type],
-
anonymous: !!params[:anonymous],
-
hide_results: (params[:hide_results] || :off),
-
discussion: discussion,
-
closed_at: 1.day.ago,
-
closing_at: 1.day.ago
-
)
-
PollService.create(poll: poll, actor: actor)
-
create_fake_stances(poll:poll)
-
outcome = fake_outcome(poll: poll)
-
-
OutcomeService.create(outcome: outcome, actor: actor, params: {recipient_emails: [observer.email]})
-
-
{ discussion: discussion,
-
group: discussion.group,
-
observer: observer,
-
actor: actor,
-
outcome: outcome,
-
title: poll.title,
-
poll: poll}
-
end
-
-
def poll_outcome_review_due_scenario(params)
-
discussion = saved(fake_discussion(group: create_group_with_members))
-
actor = discussion.group.admins.first
-
observer = fake_user
-
discussion.group.add_member! observer
-
poll = fake_poll(
-
poll_type: params[:poll_type],
-
anonymous: !!params[:anonymous],
-
hide_results: (params[:hide_results] || :off),
-
discussion: discussion,
-
closed_at: 1.day.ago,
-
closing_at: 1.day.ago
-
)
-
PollService.create(poll: poll, actor: actor)
-
create_fake_stances(poll: poll)
-
outcome = fake_outcome(poll: poll, author: poll.author, review_on: Date.today)
-
-
Events::OutcomeReviewDue.publish!(outcome)
-
# OutcomeService.create(outcome: outcome, actor: actor, params: {recipient_emails: [observer.email]})
-
-
{ discussion: discussion,
-
group: discussion.group,
-
observer: poll.author,
-
actor: actor,
-
outcome: outcome,
-
title: poll.title,
-
poll: poll}
-
end
-
-
def poll_catch_up_scenario(params)
-
discussion = saved(fake_discussion(group: create_group_with_members))
-
scenario = poll_expired_scenario(params)
-
observer = fake_user.tap(&:save!)
-
observer.email_catch_up_day = 7
-
discussion.group.add_member! observer
-
scenario[:discussion].group.add_member! observer
-
poll = scenario[:poll]
-
choices = [{poll_option_id: poll.poll_option_ids[0]}]
-
-
StanceService.create(stance: fake_stance(poll: poll, stance_choices_attributes: choices), actor: observer)
-
UserMailer.catch_up(observer.id).deliver_now
-
-
scenario.merge(observer: observer)
-
end
-
-
def alternative_poll_option_selection(poll_option_ids, i)
-
poll_option_ids.each_with_index.map {|id, j| {poll_option_id: id, score: (i+j)%3}}
-
end
-
-
def saved(obj)
-
obj.tap(&:save!)
-
end
-
-
-
end
-
module EmailHelper
-
include PrettyUrlHelper
-
def login_token(recipient, redirect_path)
-
recipient.login_tokens.create!(redirect: redirect_path)
-
end
-
-
def render_markdown(str, fmt = 'md')
-
MarkdownService.render_markdown(str, fmt)
-
end
-
-
def render_plain_text(str, fmt = 'md')
-
MarkdownService.render_plain_text(str, fmt)
-
end
-
-
def render_rich_text(str, fmt = 'md')
-
MarkdownService.render_rich_text(str, fmt)
-
end
-
-
def recipient_stance(recipient, poll)
-
poll.poll.stances.latest.find_by(participant: recipient) || Stance.new(poll: poll, participant: recipient)
-
end
-
-
def formatted_time_zone
-
ActiveSupport::TimeZone[@recipient.time_zone].to_s
-
end
-
-
def tracked_url(model, args = {})
-
args.merge!({utm_medium: 'email', utm_campaign: @event&.kind })
-
-
if model.is_a?(Poll) or model.is_a?(Outcome)
-
if stance = model.poll.stances.latest.find_by(participant: @recipient)
-
args.merge!(stance_token: stance.token)
-
end
-
end
-
-
if model.is_a?(Discussion) || model.is_a?(Comment)
-
if reader = DiscussionReader.redeemable.find_by(user: @recipient, discussion: model.discussion)
-
args.merge!(discussion_reader_token: reader.token)
-
end
-
end
-
-
polymorphic_url(model, args)
-
end
-
-
def unfollow_url(discussion, action_name, recipient, new_volume: :quiet)
-
email_actions_unfollow_discussion_url(
-
discussion_id: discussion.id,
-
utm_campaign: @event.kind,
-
utm_medium: 'email',
-
unsubscribe_token: @recipient.unsubscribe_token,
-
new_volume: new_volume
-
)
-
end
-
-
def preferences_url
-
tracked_url(email_preferences_url(unsubscribe_token: @recipient.unsubscribe_token))
-
end
-
-
def pixel_src(event)
-
email_actions_mark_discussion_as_read_url(
-
discussion_id: event.eventable.discussion.id,
-
event_id: event.id,
-
unsubscribe_token: @recipient.unsubscribe_token,
-
format: 'gif'
-
)
-
end
-
-
def mark_notification_as_read_pixel_src(notification_id)
-
email_actions_mark_notification_as_read_url(
-
id: notification_id,
-
unsubscribe_token: @recipient.unsubscribe_token,
-
format: 'gif'
-
)
-
end
-
-
def can_unfollow?(discussion, recipient)
-
DiscussionReader.for(discussion: discussion, user: recipient).volume_is_loud?
-
end
-
-
def reply_to_address(model:, user: )
-
letter = {
-
'Comment' => 'c',
-
'Poll' => 'p',
-
'Stance' => 's',
-
'Outcome' => 'o'
-
}[model.class.to_s]
-
-
address = {
-
pt: letter,
-
pi: letter ? model.id : nil,
-
d: model.discussion_id,
-
u: user.id,
-
k: user.email_api_key
-
}.compact.map { |k, v| [k,v].join('=') }.join('&')
-
[address, ENV['REPLY_HOSTNAME']].join('@')
-
end
-
-
-
def mark_summary_as_read_url_for(user, format: nil)
-
email_actions_mark_summary_email_as_read_url(unsubscribe_token: user.unsubscribe_token,
-
time_start: @time_start.utc.to_i,
-
time_finish: @time_finish.utc.to_i,
-
format: format)
-
end
-
-
def option_name(name, format, zone, date_time_pref)
-
case format
-
when 'i18n'
-
t(name)
-
when 'iso8601'
-
format_iso8601_for_humans(name, zone, date_time_pref)
-
else
-
name
-
end
-
end
-
-
def google_pie_chart_url(poll)
-
pie_chart_url(scores: proposal_sparkline(poll), colors: proposal_colors(poll))
-
end
-
-
def proposal_sparkline(poll)
-
poll.results.map {|h| h[:score] }.join(',')
-
end
-
-
def proposal_colors(poll)
-
poll.results.map{|h|h[:color]}.map{|c| c.gsub('#', '')}.join(',')
-
end
-
-
def dot_vote_stance_choice_percentage_for(stance, stance_choice)
-
max = stance.poll.dots_per_person.to_i
-
if max > 0
-
(100 * stance_choice.score.to_f / max).to_i
-
else
-
0
-
end
-
end
-
-
def score_stance_choice_percentage_for(stance, stance_choice)
-
max = stance.poll.max_score.to_i
-
if max > 0
-
(100 * stance_choice.score.to_f / max).to_i
-
else
-
0
-
end
-
end
-
-
def optional_link(url, attrs = {}, &block)
-
if url
-
content_tag(:a, {href: url}.merge(attrs)) do
-
block.call
-
end
-
else
-
content_tag(:span) do
-
block.call
-
end
-
end
-
end
-
end
-
module FormattedDateHelper
-
def format_iso8601_for_humans(str, zone, date_time_pref)
-
format_date_for_humans(parse_date_or_datetime(str), zone, date_time_pref)
-
end
-
-
def format_date_for_humans(date, zone, date_time_pref)
-
format_date_or_datetime(date.in_time_zone(zone), date_time_pref)
-
end
-
-
def is_datetime?(value)
-
value.is_a?(DateTime) or value.is_a?(Time) or value.is_a?(ActiveSupport::TimeWithZone)
-
end
-
-
def parse_date_or_datetime(value)
-
return parse_datetime(value) if is_datetime_string?(value)
-
if is_datetime?(value)
-
value
-
else
-
value.to_date
-
end
-
end
-
-
def is_datetime_string?(value)
-
!!parse_datetime(value)
-
rescue ArgumentError
-
false
-
end
-
-
def parse_datetime(value)
-
DateTime.strptime(value.sub('.000Z', 'Z'))
-
end
-
-
def format_date_or_datetime(value, date_time_pref)
-
case date_time_pref
-
when 'iso'
-
date_format = '%Y-%m-%d'
-
time_format = '%H:%M'
-
when 'day_iso'
-
date_format = '%a %Y-%m-%d'
-
time_format = '%H:%M'
-
when 'abbr'
-
date_format = '%e %b %Y'
-
time_format = '%l:%M%p'
-
when 'day_abbr'
-
date_format = '%a %e %b %Y'
-
time_format = '%l:%M%p'
-
else
-
raise "unknown date pref"
-
end
-
-
if is_datetime?(value)
-
value.strftime("#{date_format} #{time_format}")
-
else
-
value.strftime("#{date_format}")
-
end
-
end
-
end
-
module LoadAndAuthorize
-
def load_and_authorize(model, action = :show, optional: false)
-
return if optional && !(params[:"#{model}_id"] || params[:"#{model}_key"])
-
instance_variable_set :"@#{model}", ModelLocator.new(model, params).locate!
-
current_user.ability.authorize! action, instance_variable_get(:"@#{model}")
-
end
-
end
-
module LocalesHelper
-
def process_time_zone(&block)
-
Time.use_zone(TimeZoneToCity.convert(current_user.time_zone.to_s), &block)
-
end
-
-
def use_preferred_locale
-
I18n.locale = preferred_locale
-
yield if block_given?
-
save_detected_locale
-
end
-
-
def preferred_locale
-
# allow unsupported locales via params
-
normalize(params[:locale]) ||
-
first_supported_locale(user_selected_locale,
-
browser_detected_locales,
-
user_detected_locale)
-
end
-
-
def logged_out_preferred_locale
-
normalize(params[:locale]) ||
-
first_supported_locale(browser_detected_locales)
-
end
-
-
def supported_locales
-
AppConfig.locales['supported']
-
end
-
-
def save_detected_locale(user = current_user)
-
if user.is_logged_in? && browser_detected_locales.any?
-
user.update_detected_locale(first_supported_locale browser_detected_locales)
-
end
-
end
-
-
def first_supported_locale(*locales)
-
Array(locales).flatten.compact.map do |locale|
-
[normalize(locale),
-
strip_dialect(locale),
-
fallback_for(locale)].detect do |version|
-
supported_locales.include? version
-
end
-
end.compact.first || I18n.default_locale
-
end
-
-
def help_manual_locale(locale)
-
"en"
-
end
-
-
private
-
-
def normalize(locale)
-
return unless locale
-
lang, dialect = locale.to_s.sub('-', '_').split('_')
-
[lang&.downcase, dialect&.upcase].compact.join('_')
-
end
-
-
-
def strip_dialect(locale)
-
locale.to_s.split('_').first
-
end
-
-
def fallback_for(locale)
-
fallbacks[locale] || fallbacks[strip_dialect(locale)]
-
end
-
-
def fallbacks
-
AppConfig.locales['fallbacks']
-
end
-
-
def user_selected_locale
-
return nil unless current_user&.is_logged_in?
-
current_user.selected_locale
-
end
-
-
def user_detected_locale
-
return unless current_user&.is_logged_in?
-
current_user.detected_locale
-
end
-
-
def browser_detected_locales
-
parser = HttpAcceptLanguage::Parser.new(request.env["HTTP_ACCEPT_LANGUAGE"])
-
parser.user_preferred_languages.map {|locale| normalize locale }
-
end
-
end
-
module PendingActionsHelper
-
private
-
def handle_pending_actions(user = current_user)
-
if user.is_logged_in?
-
session.delete(:pending_user_id) if pending_user
-
consume_pending_login_token
-
consume_pending_identity(user)
-
consume_pending_group(user)
-
consume_pending_membership(user)
-
consume_pending_discussion_reader(user)
-
consume_pending_stance(user)
-
session.delete(:pending_login_token)
-
session.delete(:pending_identity_id)
-
session.delete(:pending_group_token)
-
session.delete(:pending_discussion_reader_token)
-
session.delete(:pending_stance_token)
-
end
-
end
-
-
def consume_pending_login_token
-
pending_login_token.update(used: true) if pending_login_token
-
end
-
-
def consume_pending_identity(user)
-
user.associate_with_identity(pending_identity) if pending_identity
-
end
-
-
def consume_pending_group(user)
-
RetryOnError.with_limit(2) do
-
if pending_group && !Membership.where(user_id: user.id, group_id: pending_group.id).exists?
-
membership = pending_group.memberships.create(user: user)
-
MembershipService.redeem(membership: membership, actor: user)
-
end
-
end
-
end
-
-
def consume_pending_membership(user)
-
if pending_membership
-
MembershipService.redeem(membership: pending_membership, actor: user)
-
end
-
end
-
-
# memberships are valid even if not accepted, but this lets us know if people are using them
-
def accept_pending_membership
-
group = @group or (@discussion && @discussion.group) or (@poll && @poll.group)
-
return unless group
-
MembershipService.redeem_if_pending!(group.membership_for(current_user))
-
end
-
-
def consume_pending_discussion_reader(user)
-
if reader = pending_discussion_reader
-
DiscussionReaderService.redeem(discussion_reader: reader, actor: user)
-
end
-
end
-
-
def consume_pending_stance(user)
-
StanceService.redeem(stance: pending_stance, actor: user) if pending_stance
-
end
-
-
def pending_group
-
Group.find_by(token: session[:pending_group_token]) if session[:pending_group_token]
-
end
-
-
def pending_login_token
-
LoginToken.find_by(token: session[:pending_login_token]) if session[:pending_login_token]
-
end
-
-
def pending_invitation
-
pending_membership || pending_discussion_reader || pending_stance
-
end
-
-
def pending_membership_token
-
params[:membership_token] || session[:pending_membership_token]
-
end
-
-
def pending_membership
-
Membership.pending.find_by(token: pending_membership_token) if pending_membership_token
-
end
-
-
def pending_discussion_reader_token
-
params[:discussion_reader_token] || session[:pending_discussion_reader_token]
-
end
-
-
def pending_discussion_reader
-
DiscussionReader.redeemable.find_by(token: pending_discussion_reader_token) if pending_discussion_reader_token
-
end
-
-
def pending_stance_token
-
params[:stance_token] || session[:pending_stance_token]
-
end
-
-
def pending_stance
-
Stance.find_by(token: pending_stance_token) if pending_stance_token
-
end
-
-
def pending_identity
-
Identities::Base.find_by(id: session[:pending_identity_id]) if session[:pending_identity_id]
-
end
-
-
def pending_user
-
User.find_by(id: session[:pending_user_id]) if session[:pending_user_id]
-
end
-
-
def serialized_pending_identity
-
Pending::TokenSerializer.new(pending_login_token, root: false).as_json ||
-
Pending::IdentitySerializer.new(pending_identity, root: false).as_json ||
-
Pending::MembershipSerializer.new(pending_membership, root: false).as_json ||
-
Pending::StanceSerializer.new(pending_stance, root: false).as_json ||
-
Pending::DiscussionReaderSerializer.new(pending_discussion_reader, root: false).as_json ||
-
Pending::GroupSerializer.new(pending_group, root: false).as_json ||
-
Pending::UserSerializer.new(pending_user, root: false).as_json || {}
-
end
-
end
-
module PrettyUrlHelper
-
include Routing
-
-
def join_url(model, opts = {})
-
super opts.merge(model: model.class.to_s.underscore, token: model.group.token)
-
end
-
-
def discussion_path(discussion, options = {})
-
super(discussion, options.merge(slug: discussion.title.parameterize))
-
end
-
-
def discussion_url(discussion, options = {})
-
super(discussion, options.merge(slug: discussion.title.parameterize))
-
end
-
-
def group_url(group, options = {})
-
if group.handle and !options.delete(:use_key)
-
group_handle_url(group.handle, options)
-
else
-
super group, options.merge(slug: group.name.parameterize)
-
end
-
end
-
-
def discussion_poll_url(model, options = {})
-
if model.discussion.present?
-
if model.is_a?(Outcome)
-
discussion_url(model.discussion, options.merge(sequence_id: model.poll.created_event.sequence_id))
-
else
-
discussion_url(model.discussion, options.merge(sequence_id: model.created_event.sequence_id))
-
end
-
else
-
poll_url(model.poll, options)
-
end
-
end
-
-
def polymorphic_url(model, opts = {})
-
case model
-
when NilClass, LoggedOutUser then nil
-
when Group, GroupIdentity then group_url(model.group, opts)
-
when PaperTrail::Version then polymorphic_url(model.item, opts)
-
when MembershipRequest then group_url(model.group, opts.merge(use_key: true))
-
when Poll then discussion_poll_url(model, opts)
-
when Outcome then discussion_poll_url(model, opts)
-
when Stance then discussion_poll_url(model, opts)
-
when Comment then comment_url(model.discussion, model, opts)
-
when Membership then membership_url(model, opts)
-
when Reaction then polymorphic_url(model.reactable, opts)
-
when ReceivedEmail then group_emails_url(model.group.key)
-
else super
-
end
-
end
-
-
def polymorphic_path(model, opts = {})
-
# angular router throws error if you give it a whole url
-
polymorphic_url(model, opts).sub(root_url, '')
-
end
-
-
def polymorphic_title(model)
-
case model
-
when PaperTrail::Version then model.item.title
-
when Comment, Discussion then model.discussion.title
-
when Poll, Outcome, Stance then model.poll.title
-
when Reaction then model.reactable.title
-
when Group then model.full_name
-
when Membership then polymorphic_title(model.group)
-
end
-
end
-
end
-
module ProtectedFromForgery
-
-
def self.included(base)
-
base.after_action :set_xsrf_token
-
end
-
-
protected
-
-
def verified_request?
-
super || Rails.env.development? || cookies['csrftoken'] == request.headers['X-CSRF-TOKEN']
-
end
-
-
private
-
-
def set_xsrf_token
-
if protect_against_forgery?
-
cookies[:csrftoken] = {
-
value: form_authenticity_token,
-
expires: 1.day.from_now,
-
secure: true
-
}
-
end
-
end
-
end
-
module SentryHelper
-
def set_sentry_context
-
Sentry.configure_scope do |scope|
-
scope.set_user(id: current_user.id, ip_address: request.remote_ip)
-
scope.set_tags(email: current_user.email, name: current_user.name)
-
# scope.set_extra(params: params.to_unsafe_h, url: request.url)
-
end
-
end
-
end
-
1
module UsesMetadata
-
1
def show
-
1
metadata
-
respond_to do |format|
-
format.html { index }
-
format.rss { render :"show.xml" }
-
format.xml
-
end
-
end
-
-
1
private
-
-
1
def metadata
-
1
@metadata ||= if current_user.can? :show, resource
-
"Metadata::#{controller_name.singularize.camelize}Serializer".constantize.new(resource)
-
else
-
{}
-
end.as_json
-
end
-
-
1
def resource
-
1
instance_variable_get("@#{resource_name}") ||
-
instance_variable_set("@#{resource_name}", ModelLocator.new(resource_name, params).locate)
-
end
-
-
1
def resource_name
-
3
controller_name.singularize
-
end
-
end
-
1
class BaseMailer < ActionMailer::Base
-
1
include ERB::Util
-
1
include ActionView::Helpers::TextHelper
-
1
include EmailHelper
-
1
include LocalesHelper
-
-
1
helper :email
-
1
helper :formatted_date
-
-
1
NOTIFICATIONS_EMAIL_ADDRESS = ENV.fetch('NOTIFICATIONS_EMAIL_ADDRESS', "notifications@#{ENV['SMTP_DOMAIN']}")
-
1
default :from => "\"#{AppConfig.theme[:site_name]}\" <#{NOTIFICATIONS_EMAIL_ADDRESS}>"
-
1
before_action :utm_hash
-
-
1
protected
-
1
def utm_hash
-
1022
@utm_hash = { utm_medium: 'email', utm_campaign: action_name }
-
end
-
-
1
def from_user_via_loomio(user)
-
992
if user.present?
-
983
"\"#{I18n.t('base_mailer.via_loomio', name: user.name, site_name: AppConfig.theme[:site_name])}\" <#{NOTIFICATIONS_EMAIL_ADDRESS}>"
-
else
-
9
"\"#{AppConfig.theme[:site_name]}\" <#{NOTIFICATIONS_EMAIL_ADDRESS}>"
-
end
-
end
-
-
1
def send_single_mail(locale: , to:, subject_key:, subject_params: {}, subject_prefix: '', subject_is_title: false, **options)
-
1021
return if NoSpam::SPAM_REGEX.match?(to)
-
1021
return if NOTIFICATIONS_EMAIL_ADDRESS == to
-
-
1021
I18n.with_locale(first_supported_locale(locale)) do
-
1021
if subject_is_title
-
28
subject = subject_prefix + subject_params[:title]
-
else
-
993
subject = subject_prefix + I18n.t(subject_key, **subject_params)
-
end
-
1021
mail options.merge(to: to, subject: subject )
-
end
-
rescue Net::SMTPSyntaxError, Net::SMTPFatalError => e
-
raise "SMTP error to: '#{to}' from: '#{options[:from]}' action: #{action_name} mailer: #{mailer_name} error: #{e}"
-
end
-
end
-
class ContactMailer < ActionMailer::Base
-
default :from => "\"#{AppConfig.theme[:site_name]}\" <#{BaseMailer::NOTIFICATIONS_EMAIL_ADDRESS}>"
-
-
def contact_message(name, email, subject, body, details = {})
-
@details = details
-
@body = body
-
mail(
-
to: ENV['SUPPORT_EMAIL'],
-
reply_to: "\"#{name}\" <#{email}>",
-
subject: subject,
-
)
-
end
-
end
-
1
class EventMailer < BaseMailer
-
1
REPLY_DELIMITER = ""*4 # surprise! this is actually U+FEFF
-
-
# TODO this should be NotificationMailer, and take a notification id
-
1
def event(recipient_id, event_id)
-
991
@current_user = @recipient = User.active.find_by!(id: recipient_id)
-
991
@event = Event.find_by!(id: event_id)
-
991
@notification = Notification.find_by(user_id: recipient_id, event_id: event_id)
-
-
991
return if @event.eventable.nil?
-
991
return if @event.eventable.respond_to?(:discarded?) && @event.eventable.discarded?
-
-
991
if %w[Poll Stance Outcome].include? @event.eventable_type
-
930
@poll = @event.eventable.poll
-
end
-
-
991
if @event.eventable.respond_to? :discussion
-
976
@discussion = @event.eventable.discussion
-
end
-
-
991
if @event.eventable.respond_to?(:group_id) && @event.eventable.group_id
-
991
@membership = Membership.active.find_by(
-
group_id: @event.eventable.group_id,
-
user_id: recipient_id
-
)
-
-
# this might be necessary to comply with anti-spam rules
-
# if someone does not respond to the invitation, don't send them more emails
-
991
return if @membership &&
-
!@recipient.email_verified &&
-
!["membership_created", "membership_resent"].include?(@event.kind)
-
end
-
-
991
@utm_hash = { utm_medium: 'email', utm_campaign: @event.kind }
-
-
thread_kinds = %w[
-
991
new_comment
-
new_discussion
-
discussion_edited
-
discussion_announced
-
]
-
-
991
headers = {
-
"Precedence": :bulk,
-
"X-Auto-Response-Suppress": :OOF,
-
"Auto-Submitted": :"auto-generated"
-
}
-
-
991
if @event.eventable.respond_to?(:calendar_invite) && @event.eventable.calendar_invite
-
2
attachments['meeting.ics'] = {
-
content_type: 'text/calendar',
-
content_transfer_encoding: 'base64',
-
content: Base64.encode64(@event.eventable.calendar_invite)
-
}
-
end
-
-
991
template_name = @event.eventable_type.tableize.singularize
-
991
template_name = 'poll' if @event.eventable_type == 'Outcome'
-
991
template_name = 'group' if @event.eventable_type == 'Membership'
-
-
# this should be notification.i18n_key
-
991
@event_key = if (@event.kind == 'user_mentioned' &&
-
@event.eventable.respond_to?(:parent) &&
-
@event.eventable.parent.present? &&
-
@event.eventable.parent.author == @recipient)
-
3
"comment_replied_to"
-
988
elsif @event.kind == 'poll_created'
-
504
'poll_announced'
-
else
-
484
@event.kind
-
end
-
-
subject_params = {
-
991
title: @event.eventable.title,
-
group_name: @event.eventable.title, # cope for old translations
-
poll_type: @poll && I18n.t("poll_types.#{@poll.poll_type}"),
-
actor: @event.user.name,
-
site_name: AppConfig.theme[:site_name]
-
}
-
-
991
send_single_mail(
-
to: @recipient.email,
-
from: from_user_via_loomio(@event.user),
-
locale: @recipient.locale,
-
reply_to: reply_to_address_with_group_name(model: @event.eventable, user: @recipient),
-
subject_prefix: group_name_prefix(@event),
-
subject_key: "notifications.with_title.#{@event_key}",
-
subject_params: subject_params,
-
subject_is_title: thread_kinds.include?(@event.kind),
-
template_name: template_name
-
)
-
end
-
-
1
private
-
1
def group_name_prefix(event)
-
991
model = event.eventable
-
991
if %w[membership_requested membership_created].include? event.kind
-
11
''
-
else
-
980
model.group.present? ? "[#{model.group.handle || model.group.full_name}] " : ''
-
end
-
end
-
-
1
def reply_to_address_with_group_name(model:, user:)
-
991
return nil unless user.is_logged_in?
-
991
return nil unless model.respond_to?(:discussion) && model.discussion.present?
-
953
if model.discussion.group.present?
-
953
"\"#{I18n.transliterate(model.discussion.group.full_name).truncate(50).delete('"')}\" <#{reply_to_address(model: model, user: user)}>"
-
else
-
"\"#{user.name}\" <#{reply_to_address(model: model, user: user)}>"
-
end
-
end
-
end
-
1
class ForwardMailer < ActionMailer::Base
-
1
layout nil
-
-
1
def forward_message(to:, from:, reply_to:, subject:, body_text: nil, body_html: nil)
-
1
@body_text = body_text
-
1
@body_html = body_html
-
1
mail(
-
from: from,
-
to: to,
-
reply_to: reply_to,
-
subject: subject,
-
layout: nil
-
)
-
end
-
end
-
1
class GroupMailer < BaseMailer
-
1
def destroy_warning(group_id, recipient_id, deletor_id)
-
2
@group = Group.find(group_id)
-
2
@recipient = User.find(recipient_id)
-
2
@deletor = User.find(deletor_id)
-
-
2
send_single_mail to: @recipient.name_and_email,
-
reply_to: ENV['SUPPORT_EMAIL'],
-
subject_key: "group_mailer.destroy_warning.subject",
-
locale: @recipient.locale
-
end
-
end
-
1
class TaskMailer < BaseMailer
-
1
def task_due_reminder(recipient, task)
-
1
@recipient = recipient
-
1
@task = task
-
1
send_single_mail(
-
locale: @recipient.locale,
-
to: @recipient.name_and_email,
-
subject_key: 'task_mailer.task_due_reminder.subject',
-
subject_params: {name: @task.name},
-
)
-
end
-
end
-
1
class UserMailer < BaseMailer
-
1
def redacted(email, locale)
-
4
send_single_mail to: email,
-
subject_key: "user_mailer.redacted.subject",
-
subject_params: { site_name: AppConfig.theme[:site_name] },
-
locale: locale
-
end
-
-
1
def accounts_merged(user_id)
-
2
@user = User.find(user_id)
-
2
@token = @user.login_tokens.create!
-
2
send_single_mail to: @user.email,
-
subject_key: "user_mailer.accounts_merged.subject",
-
subject_params: { site_name: AppConfig.theme[:site_name] },
-
locale: @user.locale
-
end
-
-
1
def merge_verification(source_user:, target_user:, hash:)
-
@source_user = source_user
-
@target_user = target_user
-
@hash = hash
-
send_single_mail to: @target_user.email,
-
subject_key: "user_mailer.merge_verification.subject",
-
subject_params: {site_name: AppConfig.theme[:site_name]},
-
locale: @target_user.locale
-
end
-
-
1
def catch_up(user_id, time_since = nil, frequency = 'daily')
-
4
user = User.find(user_id)
-
4
return unless user.email_catch_up_day
-
4
@current_user = @recipient = @user = user
-
-
4
if frequency == 'daily'
-
3
@time_start = time_since || 24.hours.ago
-
1
elsif frequency == 'other'
-
@time_start = time_since || 48.hours.ago
-
else
-
1
@time_start = time_since || 1.week.ago
-
end
-
-
4
@time_finish = Time.zone.now
-
4
@time_frame = @time_start...@time_finish
-
-
4
@discussions = DiscussionQuery.visible_to(
-
user: user,
-
only_unread: true,
-
or_public: false,
-
or_subgroups: false).last_activity_after(@time_start)
-
4
@groups = @user.groups.order(full_name: :asc)
-
-
4
@cache = RecordCache.for_collection(@discussions, user_id)
-
-
4
@subject_key = "email.catch_up.#{frequency}_subject"
-
4
@subject_params = { site_name: AppConfig.theme[:site_name] }
-
-
4
unless @discussions.empty?
-
3
@discussions_by_group_id = @discussions.group_by(&:group_id)
-
3
send_single_mail to: @user.email,
-
subject_key: @subject_key,
-
subject_params: @subject_params,
-
locale: @user.locale
-
end
-
end
-
-
1
def membership_request_approved(recipient_id, event_id)
-
5
@user = User.find_by(id: recipient_id)
-
5
@group = Event.find_by(id: event_id).eventable.group
-
-
5
send_single_mail to: @user.email,
-
reply_to: @group.admin_email,
-
subject_key: "email.group_membership_approved.subject",
-
subject_params: {group_name: @group.full_name},
-
locale: @user.locale
-
end
-
-
1
def user_added_to_group(recipient_id, event_id)
-
1
@user = User.find_by!(id: recipient_id)
-
1
event = Event.find_by!(id: event_id)
-
1
@group = event.eventable.group
-
1
@inviter = event.eventable.inviter || @group.admins.first
-
-
1
send_single_mail to: @user.email,
-
from: from_user_via_loomio(@inviter),
-
reply_to: @inviter.try(:name_and_email),
-
subject_key: "email.user_added_to_group.subject",
-
subject_params: { which_group: @group.full_name, who: @inviter.name, site_name: AppConfig.theme[:site_name] },
-
locale: [@user.locale, @inviter.locale]
-
end
-
-
1
def group_export_ready(recipient_id, group_name, document_id)
-
1
@user = User.find(recipient_id)
-
1
@document = Document.find(document_id)
-
1
send_single_mail to: @user.email,
-
subject_key: "user_mailer.group_export_ready.subject",
-
subject_params: {group_name: group_name},
-
locale: @user.locale
-
end
-
-
1
def login(user_id, token_id)
-
11
@user = User.find_by!(id: user_id)
-
11
@token = LoginToken.find_by!(id: token_id)
-
11
send_single_mail to: @user.email,
-
subject_key: "email.login.subject",
-
subject_params: {site_name: AppConfig.theme[:site_name]},
-
locale: @user.locale
-
end
-
-
1
def contact_request(contact_request:)
-
@contact_request = contact_request
-
-
send_single_mail to: @contact_request.recipient.email,
-
from: from_user_via_loomio(@contact_request.sender),
-
reply_to: @contact_request.sender.name_and_email,
-
subject_key: "email.contact_request.subject",
-
subject_params: { name: @contact_request.sender.name,
-
site_name: AppConfig.theme[:site_name]},
-
locale: [@contact_request.recipient.locale, @contact_request.sender.locale]
-
end
-
-
end
-
1
module Ability::Attachment
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can :show, ::Attachment do |attachment|
-
user.groups.exists?(attachment.record.group.id)
-
end
-
-
1713
can :destroy, ::Attachment do |attachment|
-
2
user.adminable_groups.exists?(attachment.record.group.id)
-
end
-
end
-
end
-
1
module Ability
-
1
class Base
-
1
include CanCan::Ability
-
1
prepend Ability::Comment
-
1
prepend Ability::DiscussionReader
-
1
prepend Ability::Discussion
-
1
prepend Ability::Document
-
1
prepend Ability::Group
-
1
prepend Ability::Identity
-
1
prepend Ability::MembershipRequest
-
1
prepend Ability::Membership
-
1
prepend Ability::Outcome
-
1
prepend Ability::Poll
-
1
prepend Ability::Reaction
-
1
prepend Ability::Stance
-
1
prepend Ability::User
-
1
prepend Ability::Tag
-
1
prepend Ability::Event
-
1
prepend Ability::Chatbot
-
1
prepend Ability::Attachment
-
1
prepend Ability::Task
-
1
prepend Ability::PollTemplate
-
1
prepend Ability::DiscussionTemplate
-
-
1
def initialize(user)
-
1713
@user = user
-
1713
can(:subscribe_to, GlobalMessageChannel) { true }
-
end
-
-
1
private
-
-
1
def user_is_member_of?(group_id)
-
21
@user.memberships.find_by(group_id: group_id)
-
end
-
-
1
def user_is_admin_of?(group_id)
-
32
@user.admin_memberships.find_by(group_id: group_id)
-
end
-
-
1
def user_is_member_of_any?(groups)
-
@user.memberships.find_by(group: groups)
-
end
-
-
1
def user_is_admin_of_any?(groups)
-
@user.admin_memberships.find_by(group: groups)
-
end
-
-
1
def user_is_author_of?(object)
-
2
@user.is_logged_in? && @user.id == object.author_id
-
end
-
-
end
-
end
-
1
module Ability::Chatbot
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can [:create, :destroy, :update, :test], ::Chatbot do |chatbot|
-
chatbot.group.admins.exists?(user.id)
-
end
-
end
-
end
-
1
module Ability::Comment
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can [:create], ::Comment do |comment|
-
191
comment.discussion &&
-
!comment.discussion.closed_at &&
-
comment.discussion.members.exists?(user.id)
-
end
-
-
1713
can [:update], ::Comment do |comment|
-
13
!comment.discussion.closed_at && (
-
13
(comment.discussion.members.exists?(user.id) && comment.author == user && comment.group.members_can_edit_comments) ||
-
6
(comment.discussion.admins.exists?(user.id) && comment.group.admins_can_edit_user_content)
-
)
-
end
-
-
1713
can [:discard, :undiscard], ::Comment do |comment|
-
3
!comment.discussion.closed_at &&
-
(
-
3
(comment.author == user && comment.discussion.members.exists?(user.id)) ||
-
comment.discussion.admins.exists?(user.id)
-
)
-
end
-
-
1713
can [:destroy], ::Comment do |comment|
-
9
!comment.discussion.closed_at &&
-
Comment.where(parent: comment).count == 0 &&
-
(
-
8
comment.discussion.admins.exists?(user.id) ||
-
4
(comment.author == user &&
-
comment.discussion.members.exists?(user.id) &&
-
comment.group.members_can_delete_comments)
-
)
-
end
-
-
1713
can [:show], ::Comment do |comment|
-
9
can?(:show, comment.discussion) && comment.kept?
-
end
-
end
-
end
-
1
module Ability::Discussion
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can [:show,
-
:print,
-
:dismiss,
-
:subscribe_to], ::Discussion do |discussion|
-
114
DiscussionQuery.visible_to(user: user).exists?(discussion.id)
-
end
-
-
1713
can [:mark_as_read, :mark_as_seen], ::Discussion do |discussion|
-
5
user.is_logged_in? && can?(:show, discussion)
-
end
-
-
1713
can :update_version, ::Discussion do |discussion|
-
discussion.author == user or discussion.admins.exists?(user.id)
-
end
-
-
1713
can :create, ::Discussion do |discussion|
-
375
user.email_verified? &&
-
(
-
375
discussion.group.blank? ||
-
discussion.group.admins.exists?(user.id) ||
-
266
(discussion.group.members_can_start_discussions && discussion.group.members.exists?(user.id))
-
)
-
end
-
-
1713
can [:announce], ::Discussion do |discussion|
-
13
if discussion.group_id
-
10
discussion.group.admins.exists?(user.id) ||
-
8
(discussion.group.members_can_announce && discussion.members.exists?(user.id))
-
else
-
3
discussion.admins.exists?(user.id)
-
end
-
end
-
-
1713
can [:add_members], ::Discussion do |discussion|
-
814
discussion.members.exists?(user.id)
-
end
-
-
1713
can [:add_guests], ::Discussion do |discussion|
-
824
if discussion.group_id
-
796
Subscription.for(discussion.group).allow_guests &&
-
796
(discussion.group.admins.exists?(user.id) || (discussion.group.members_can_add_guests && discussion.members.exists?(user.id)))
-
else
-
28
!discussion.id || discussion.admins.exists?(user.id)
-
end
-
end
-
-
1713
can [:update, :move, :move_comments, :pin], ::Discussion do |discussion|
-
63
discussion.discarded_at.nil? &&
-
63
(discussion.author == user ||
-
discussion.admins.exists?(user.id) ||
-
23
(discussion.group.members_can_edit_discussions && discussion.members.exists?(user.id)))
-
end
-
-
1713
can [:destroy, :discard], ::Discussion do |discussion|
-
5
discussion.discarded_at.nil? &&
-
5
(discussion.author == user || discussion.admins.exists?(user.id))
-
end
-
-
1713
can [:set_volume], ::Discussion do |discussion|
-
discussion.members.exists?(user.id)
-
end
-
-
1713
can :remove_events, ::Discussion do |discussion|
-
discussion.author == user or discussion.admins.exists?(user.id)
-
end
-
end
-
end
-
1
module Ability::DiscussionReader
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can [:update], ::DiscussionReader do |discussion_reader|
-
discussion_reader.user.id == user.id
-
end
-
-
1713
can [:redeem], ::DiscussionReader do |discussion_reader|
-
DiscussionReader.redeemable.exists?(discussion_reader.id)
-
end
-
-
1713
can [:make_admin, :remove_admin, :resend], ::DiscussionReader do |discussion_reader|
-
discussion_reader.discussion.admins.exists?(user.id)
-
end
-
-
1713
can [:remove], ::DiscussionReader do |discussion_reader|
-
discussion_reader.guest && discussion_reader.discussion.admins.exists?(user.id)
-
end
-
end
-
end
-
1
module Ability::DiscussionTemplate
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can([:create, :update], ::DiscussionTemplate) do |discussion_template|
-
discussion_template.group.admins.exists?(user.id)
-
end
-
end
-
end
-
1
module Ability::Document
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can [:create, :update], ::Document do |document|
-
if document.model.presence
-
user.can? :update, document.model
-
else
-
user.email_verified?
-
end
-
end
-
-
1713
can :destroy, ::Document do |document|
-
user_is_admin_of? document.model.group.id
-
end
-
end
-
end
-
1
module Ability
-
1
module Event
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can [:pin, :unpin], ::Event do |event|
-
1
event.discussion && can?(:update, event.discussion)
-
end
-
end
-
end
-
end
-
1
module Ability::Group
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can [:show], ::Group do |group|
-
106
!group.archived_at &&
-
(
-
106
group.is_visible_to_public? or
-
group.members.exists?(user.id) or
-
12
(group.is_visible_to_parent_members? and user_is_member_of?(group.parent_id)) or
-
10
(user.group_token && user.group_token == group.token) or
-
9
(user.membership_token && group.memberships.pending.find_by(token: user.membership_token))
-
)
-
end
-
-
1713
can [:see_private_content, :subscribe_to], ::Group do |group|
-
10
!group.archived_at && (
-
10
group.group_privacy == 'open' or
-
group.members.exists?(user.id) or
-
2
(group.is_visible_to_parent_members? and group.parent_or_self.members.exists?(user.id)))
-
end
-
-
1713
can [:update,
-
:email_members,
-
:archive,
-
:destroy,
-
:publish,
-
:export,
-
:view_pending_invitations], ::Group do |group|
-
20
group.admins.exists?(user.id)
-
end
-
-
1713
can [:members_autocomplete,
-
:show_chatbots,
-
:set_volume], ::Group do |group|
-
user.email_verified? && group.members.exists?(user.id)
-
end
-
-
1713
can [:move_discussions_to], ::Group do |group|
-
9
user.email_verified? &&
-
9
(group.admins.exists?(user.id) ||
-
9
(group.members_can_start_discussions? && group.members.exists?(user.id)))
-
end
-
-
1713
can [:add_guests], ::Group do |group|
-
15
user.email_verified? && Subscription.for(group).is_active? &&
-
15
((group.members_can_add_guests && group.members.exists?(user.id)) || group.admins.exists?(user.id))
-
end
-
-
1713
can [:add_members,
-
:invite_people,
-
:announce,
-
:manage_membership_requests], ::Group do |group|
-
55
user.is_admin ||
-
(
-
51
((group.members_can_add_members? && group.members.exists?(user.id)) ||
-
group.admins.exists?(user.id))
-
)
-
end
-
-
1713
can [:notify], ::Group do |group|
-
(group.members_can_announce && group.members.exists?(user.id)) || group.admins.exists?(user.id)
-
end
-
-
# please note that I don't like this duplication either.
-
# add_subgroup checks against a parent group
-
1713
can [:add_subgroup], ::Group do |group|
-
2
user.email_verified? &&
-
2
(group.is_parent? &&
-
group.members.exists?(user.id) &&
-
2
(group.members_can_create_subgroups? || group.admins.exists?(user.id)))
-
end
-
-
1713
can :move, ::Group do |group|
-
2
user.is_admin
-
end
-
-
# create group checks against the group to be created
-
1713
can :create, ::Group do |group|
-
# anyone can create a top level group of their own
-
# otherwise, the group must be a subgroup
-
# inwhich case we need to confirm membership and permission
-
11
(user.is_admin or AppConfig.app_features[:create_group]) &&
-
user.email_verified? &&
-
group.is_parent? ||
-
3
( user_is_admin_of?(group.parent_id) ||
-
3
(user_is_member_of?(group.parent_id) && group.parent.members_can_create_subgroups?) )
-
end
-
-
1713
can :join, ::Group do |group|
-
1
(user.email_verified? && can?(:show, group) && group.membership_granted_upon_request?) ||
-
1
(user_is_admin_of?(group.parent_id) && can?(:show, group) && group.membership_granted_upon_approval?)
-
end
-
-
1713
can :merge, ::Group do |group|
-
3
user.is_admin?
-
end
-
end
-
end
-
1
module Ability::Identity
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can [:show, :destroy], ::Identities::Base do |identity|
-
user.identities.exists? identity.id
-
end
-
end
-
end
-
1
module Ability::Membership
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can :show, ::Membership do |membership|
-
membership.user_id == user.id || membership.group.admins.exists?(user.id) || membership.inviter_id == user.id
-
end
-
-
1713
can [:update], ::Membership do |membership|
-
6
membership.user_id == user.id || membership.group.admins.exists?(user.id)
-
end
-
-
1713
can [:make_admin], ::Membership do |membership|
-
6
membership.group.admins.exists?(user.id) ||
-
5
(user_is_member_of?(membership.group_id) && membership.user == user && membership.group.admin_memberships.count == 0) ||
-
4
(user_is_admin_of?(membership.group.parent_id) && user == membership.user)
-
end
-
-
1713
can :resend, ::Membership do |membership|
-
3
!membership.accepted_at? && user_is_admin_of?(membership.group_id)
-
end
-
-
1713
can [:remove_admin, :revoke, :destroy], ::Membership do |membership|
-
14
user.is_admin || (
-
13
(membership.user == user ||
-
user_is_admin_of?(membership.group_id) ||
-
5
(membership.inviter == user && !membership.accepted_at?))
-
)
-
end
-
end
-
end
-
1
module Ability::MembershipRequest
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can :create, ::MembershipRequest do |request|
-
group = request.group
-
can?(:show, group) and group.membership_granted_upon_approval?
-
end
-
-
1713
can :cancel, ::MembershipRequest, requestor_id: user.id
-
-
1713
can [:show, :approve, :ignore], ::MembershipRequest do |membership_request|
-
12
group = membership_request.group
-
-
12
user_is_admin_of?(group.id) or
-
10
(user_is_member_of?(group.id) and group.members_can_add_members?)
-
end
-
end
-
end
-
1
module Ability::Outcome
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can :show, ::Outcome do |outcome|
-
146
can? :show, outcome.poll
-
end
-
-
1713
can [:create, :update], ::Outcome do |outcome|
-
34
!outcome.poll.active? &&
-
31
(outcome.admins.exists?(user.id) || (outcome.group.members_can_edit_discussions && outcome.members.exists?(user.id)))
-
end
-
-
1713
can [:announce], ::Outcome do |outcome|
-
22
!outcome.poll.active? && can?(:announce, outcome.poll)
-
end
-
-
1713
can [:add_members], ::Outcome do |outcome|
-
96
!outcome.poll.active? && can?(:add_members, outcome.poll)
-
end
-
-
1713
can [:add_guests], ::Outcome do |outcome|
-
84
!outcome.poll.active? && can?(:add_guests, outcome.poll)
-
end
-
end
-
end
-
1
module Ability::Poll
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can :vote_in, ::Poll do |poll|
-
91
user.is_logged_in? &&
-
poll.active? &&
-
(
-
88
poll.unmasked_voters.exists?(user.id) ||
-
21
(!poll.specified_voters_only && poll.members.exists?(user.id))
-
)
-
end
-
-
1713
can [:export], ::Poll do |poll|
-
3
user.can?(:show, poll) && poll.show_results?
-
end
-
-
1713
can [:show], ::Poll do |poll|
-
268
PollQuery.visible_to(user: user, show_public: true).exists?(poll.id)
-
end
-
-
1713
can [:create], ::Poll do |poll|
-
174
(poll.poll_template_id.nil? || poll.poll_template.public? || user.group_ids.include?(poll.poll_template.group_id)) &&
-
174
(poll.discussion_id.nil? || !poll.discussion.closed_at) &&
-
(
-
174
(poll.group_id &&
-
(
-
170
(poll.group.admins.exists?(user.id) || # user is admin
-
40
(poll.group.members_can_raise_motions && poll.group.members.exists?(user.id)) || # user is member
-
7
(poll.group.members_can_raise_motions && poll.discussion.present? && poll.discussion.guests.exists?(user.id)))
-
)
-
) ||
-
10
(poll.group_id.nil? && poll.discussion_id && poll.discussion.members.exists?(user.id)) ||
-
10
(poll.group_id.nil? && poll.discussion_id.nil? && user.is_logged_in? && user.email_verified?)
-
)
-
end
-
-
1713
can [:announce, :remind], ::Poll do |poll|
-
38
if poll.group_id
-
35
poll.group.admins.exists?(user.id) ||
-
24
(poll.group.members_can_announce && poll.admins.exists?(user.id))
-
else
-
3
poll.admins.exists?(user.id) ||
-
2
(!poll.specified_voters_only && poll.members.exists?(user.id))
-
end
-
end
-
-
1713
can [:add_voters, :add_members], ::Poll do |poll|
-
1073
poll.admins.exists?(user.id)
-
end
-
-
1713
can [:add_guests], ::Poll do |poll|
-
932
if poll.group_id
-
911
Subscription.for(poll.group).allow_guests &&
-
911
(poll.group.admins.exists?(user.id) || (poll.group.members_can_add_guests && poll.admins.exists?(user.id)))
-
else
-
21
poll.admins.exists?(user.id)
-
end
-
end
-
-
1713
can [:update], ::Poll do |poll|
-
23
(poll.discussion_id.blank? || !poll.discussion.closed_at) &&
-
poll.admins.exists?(user.id)
-
end
-
-
1713
can [:destroy], ::Poll do |poll|
-
3
(poll.discussion_id.blank? || !poll.discussion.closed_at) &&
-
poll.admins.exists?(user.id)
-
end
-
-
1713
can :close, ::Poll do |poll|
-
10
poll.active? &&
-
poll.admins.exists?(user.id)
-
end
-
-
1713
can :reopen, ::Poll do |poll|
-
4
poll.closed? &&
-
!poll.anonymous &&
-
can?(:update, poll)
-
end
-
end
-
end
-
1
module Ability::PollTemplate
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can([:create, :update], ::PollTemplate) do |poll_template|
-
poll_template.group.admins.exists?(user.id)
-
end
-
end
-
end
-
1
module Ability::Reaction
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can :show, ::Reaction do |reaction|
-
can?(:show, reaction.reactable)
-
end
-
-
1713
can :update, ::Reaction do |reaction|
-
5
user.is_logged_in? && can?(:show, reaction.reactable)
-
end
-
-
1713
can :destroy, ::Reaction do |reaction|
-
2
user_is_author_of?(reaction)
-
end
-
end
-
end
-
1
module Ability::Stance
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can :show, ::Stance do |stance|
-
user.can?(:show, stance.poll)
-
end
-
-
1713
can [:update], ::Stance do |stance|
-
54
can?(:vote_in, stance.poll) &&
-
stance.real_participant == user &&
-
stance.latest?
-
end
-
-
1713
can [:uncast], ::Stance do |stance|
-
2
can?(:update, stance) && stance.cast_at.present?
-
end
-
-
1713
can [:create], ::Stance do |stance|
-
2
user.can? :vote_in, stance.poll
-
end
-
-
1713
can :redeem, ::Stance do |stance|
-
Stance.redeemable.exists?(stance.id)
-
end
-
-
1713
can [:make_admin, :remove_admin, :resend, :remove], ::Stance do |stance|
-
6
stance.poll.admins.exists?(user.id)
-
end
-
-
end
-
end
-
1
module Ability
-
1
module Tag
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can [:create, :update, :destroy], ::Tag do |tag|
-
6
tag.group.parent_or_self.admins.exists? user.id
-
end
-
-
1713
can :show, ::Tag do |tag|
-
user.can? :show, tag.group
-
end
-
end
-
end
-
end
-
1
module Ability::Task
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can [:update], ::Task do |task|
-
5
(task.author_id == user.id) || can?(:update, task.record) || (task.users.exists? user.id)
-
end
-
end
-
end
-
1
module Ability::User
-
1
def initialize(user)
-
1713
super(user)
-
-
1713
can :show, ::User do |u|
-
4
user.is_logged_in? && u.deactivated_at.nil?
-
end
-
-
1713
can [:update,
-
:see_notifications_for,
-
:subscribe_to], ::User do |u|
-
6
user == u
-
end
-
-
1713
can [:deactivate], ::User do |u|
-
3
(user == u || user.is_admin?) && u.deactivated_at.nil?
-
end
-
-
1713
can [:redact], ::User do |u|
-
4
(user == u || user.is_admin?) && !u.email.nil?
-
end
-
-
1713
can [:contact], ::User do |u|
-
5
ContactableQuery.contactable(actor: user, user: u)
-
end
-
end
-
end
-
1
class AnonymousUser < LoggedOutUser
-
1
def name
-
425
I18n.t(:'common.anonymous')
-
end
-
-
1
def username
-
:anonymous
-
end
-
-
1
def name_or_username
-
9
name || username
-
end
-
-
1
def avatar_kind
-
'initials'
-
end
-
-
1
def avatar_initials
-
"👤"
-
end
-
end
-
class ApplicationRecord < ActiveRecord::Base
-
self.abstract_class = true
-
end
-
1
class Attachment < ActiveStorage::Attachment
-
end
-
class BlockedDomain < ApplicationRecord
-
end
-
module Boot
-
class Site
-
include LocalesHelper
-
include Routing
-
-
def payload
-
@payload ||= {
-
version: Loomio::Version.current,
-
release: AppConfig.release,
-
systemNotice: ENV['LOOMIO_SYSTEM_NOTICE'],
-
environment: Rails.env,
-
permittedParams: PermittedParamsSerializer.new({}),
-
locales: ActiveModel::ArraySerializer.new(supported_locales, each_serializer: LocaleSerializer, root: false),
-
defaultLocale: I18n.locale,
-
newsletterEnabled: ENV['NEWSLETTER_ENABLED'],
-
recaptchaKey: ENV['RECAPTCHA_APP_KEY'],
-
baseUrl: root_url,
-
contactEmail: ENV['SUPPORT_EMAIL'],
-
plugins: { installed: [], outlets: [], routes: [] },
-
theme: AppConfig.theme,
-
sentry_dsn: ENV['SENTRY_PUBLIC_DSN'],
-
plausible_src: ENV['PLAUSIBLE_SRC'],
-
plausible_site: ENV['PLAUSIBLE_SITE'],
-
features: {
-
app: AppConfig.app_features
-
},
-
inlineTranslation: {
-
isAvailable: TranslationService.available?
-
},
-
pollTypes: AppConfig.poll_types,
-
pollColors: AppConfig.colors,
-
webhookEventKinds: AppConfig.webhook_event_kinds,
-
identityProviders: AppConfig.providers.fetch('identity', []).map do |provider|
-
({ name: provider, href: send("#{provider}_oauth_path") } if ENV["#{provider.upcase}_APP_KEY"])
-
end.compact
-
}
-
end
-
end
-
end
-
1
module Boot
-
1
class User
-
1
attr_reader :user
-
-
1
def initialize(user, identity: {}, flash: {}, channel_token: nil)
-
6
@user = user
-
6
@identity = identity
-
6
@flash = flash.to_h
-
6
@channel_token = channel_token
-
end
-
-
1
def payload
-
6
@payload ||= user_payload.merge(
-
current_user_id: user.id,
-
pending_identity: @identity,
-
flash: @flash,
-
channel_token: @channel_token
-
)
-
end
-
-
1
private
-
-
1
def user_payload
-
6
ActiveModel::ArraySerializer.new(Array(@user),
-
6
each_serializer: (user.restricted ? Restricted::UserSerializer : CurrentUserSerializer),
-
root: :users
-
).as_json
-
end
-
end
-
end
-
1
class CalendarInvite
-
1
include PrettyUrlHelper
-
1
include FormattedDateHelper
-
-
1
def initialize(outcome = Outcome.new)
-
8
@calendar = build_calendar(outcome)
-
end
-
-
1
def to_ical
-
8
@calendar&.to_ical
-
end
-
-
1
private
-
-
1
def build_calendar(outcome)
-
8
Icalendar::Calendar.new.tap do |calendar|
-
8
calendar.event do |event|
-
8
if outcome.poll_option.name.match /^\d+-\d+-\d+$/
-
8
event.duration = "+P0W1D0H0M"
-
8
event.dtstart = outcome.poll_option.name.to_date
-
else
-
event.duration = "+P0W0D0H#{outcome.poll.meeting_duration}M"
-
event.dtstart = Icalendar::Values::DateTime.new(parse_datetime(outcome.poll_option.name), tzid: 'UTC')
-
end
-
8
event.organizer = Icalendar::Values::CalAddress.new(outcome.author.email, cn: outcome.author.name)
-
8
event.summary = outcome.event_summary
-
8
event.description = outcome.event_description
-
8
event.location = outcome.event_location
-
8
event.attendee = outcome.attendee_emails
-
8
event.ip_class = "PRIVATE"
-
8
event.url = poll_url(outcome.poll)
-
end
-
end
-
end
-
end
-
1
class Chatbot < ApplicationRecord
-
1
belongs_to :group
-
1
belongs_to :author, class_name: 'User'
-
-
1
validates_presence_of :server
-
1
validates_presence_of :name
-
1
validates_inclusion_of :kind, in: ['matrix', 'webhook']
-
1
validates_inclusion_of :webhook_kind, in: ['slack', 'microsoft', 'discord', 'markdown', nil]
-
-
1
def config
-
{
-
# id: self.id,
-
server: self.server,
-
access_token: self.access_token,
-
channel: self.channel
-
}
-
end
-
end
-
1
class Comment < ApplicationRecord
-
1
include Discard::Model
-
1
include CustomCounterCache::Model
-
1
include Translatable
-
1
include Reactable
-
1
include HasMentions
-
1
include HasCreatedEvent
-
1
include HasEvents
-
1
include HasRichText
-
1
include Searchable
-
-
1
def self.pg_search_insert_statement(id: nil, author_id: nil, discussion_id: nil)
-
471
content_str = "regexp_replace(CONCAT_WS(' ', comments.body, users.name), E'<[^>]+>', '', 'gi')"
-
471
<<~SQL.squish
-
INSERT INTO pg_search_documents (
-
searchable_type,
-
searchable_id,
-
group_id,
-
discussion_id,
-
author_id,
-
authored_at,
-
content,
-
ts_content,
-
created_at,
-
updated_at)
-
SELECT 'Comment' AS searchable_type,
-
comments.id AS searchable_id,
-
discussions.group_id as group_id,
-
discussions.id AS discussion_id,
-
comments.user_id AS author_id,
-
comments.created_at AS authored_at,
-
#{content_str} AS content,
-
to_tsvector('simple',#{content_str}) as ts_content,
-
now() AS created_at,
-
now() AS updated_at
-
FROM comments
-
LEFT JOIN discussions ON discussions.id = comments.discussion_id
-
LEFT JOIN users ON users.id = comments.user_id
-
#{discussion_id ? "LEFT JOIN events ON events.eventable_type = 'Comment' AND events.eventable_id = comments.id" : ""}
-
WHERE comments.discarded_at IS NULL
-
AND discussions.discarded_at IS NULL
-
#{id ? " AND comments.id = #{id.to_i} LIMIT 1" : ""}
-
#{author_id ? " AND comments.user_id = #{author_id.to_i}" : ""}
-
#{discussion_id ? " AND events.discussion_id = #{discussion_id.to_i}" : ""}
-
SQL
-
end
-
-
1
has_paper_trail only: [:body, :body_format, :user_id, :discarded_at, :discarded_by]
-
-
1
is_translatable on: :body
-
1
is_mentionable on: :body
-
1
is_rich_text on: :body
-
-
1
belongs_to :discussion
-
1
belongs_to :user
-
1
belongs_to :parent, polymorphic: true
-
-
1
has_many :documents, as: :model, dependent: :destroy
-
-
1
validates_presence_of :user, unless: :discarded_at
-
-
1
validate :parent_comment_belongs_to_same_discussion
-
1
validate :has_body_or_attachment
-
-
1
alias_attribute :author, :user
-
1
alias_attribute :author_id, :user_id
-
-
1
scope :dangling, -> { joins('left join discussions on discussion_id = discussions.id').where('discussion_id is not null and discussions.id is null') }
-
1
scope :in_organisation, ->(group) { includes(:user, :discussion).joins(:discussion).where("discussions.group_id": group.id_and_subgroup_ids) }
-
-
1
before_validation :assign_parent_if_nil
-
-
1
delegate :name, to: :user, prefix: :user
-
1
delegate :name, to: :user, prefix: :author
-
1
delegate :email, to: :user, prefix: :user
-
1
delegate :author, to: :parent, prefix: :parent, allow_nil: true
-
1
delegate :group, to: :discussion
-
1
delegate :group_id, to: :discussion, allow_nil: true
-
1
delegate :full_name, to: :group, prefix: :group
-
1
delegate :locale, to: :user
-
1
delegate :mailer, to: :discussion
-
1
delegate :guests, to: :discussion
-
1
delegate :members, to: :discussion
-
1
delegate :title, to: :discussion
-
-
6
define_counter_cache(:versions_count) { |comment| comment.versions.count }
-
-
1
def real_participant
-
author
-
end
-
-
1
def assign_parent_if_nil
-
623
self.parent = self.discussion if self.parent_id.nil?
-
end
-
-
1
def poll
-
nil
-
end
-
-
1
def poll_id
-
nil
-
end
-
-
1
def user
-
2488
super || AnonymousUser.new
-
end
-
-
1
def should_pin
-
183
return false if body_format != "html"
-
2
Nokogiri::HTML(self.body).css("h1,h2,h3").length > 0
-
end
-
-
1
def parent_event
-
246
if parent.nil? && discussion.present?
-
self.parent = self.discussion
-
save!(validate: false)
-
end
-
-
246
if parent.is_a? Stance
-
# if stance, the could be updated event. sucks i know
-
Event.where(eventable_type: parent_type, eventable_id: parent_id).where('discussion_id is not null').first
-
else
-
246
parent.created_event
-
end
-
end
-
-
1
def created_event_kind
-
42
:new_comment
-
end
-
-
1
def is_most_recent?
-
2
discussion.comments.last == self
-
end
-
-
1
def is_edited?
-
edited_at.present?
-
end
-
-
1
private
-
-
1
def has_body_or_attachment
-
623
if !discarded_at && body_blank? && files.empty? && image_files.empty?
-
3
errors.add(:body, I18n.t(:"activerecord.errors.messages.blank"))
-
end
-
end
-
-
1
def body_blank?
-
617
body.to_s.empty? || body.to_s == "<p></p>"
-
end
-
-
1
def parent_comment_belongs_to_same_discussion
-
# if someone replies to a deleted comment (in practice, by email), reparent to the discussion
-
623
self.parent = self.discussion if parent.nil? && discussion.present?
-
-
623
unless discussion_id == parent.discussion_id
-
6
errors.add(:parent, "Needs to have same discussion id")
-
end
-
end
-
end
-
module AvatarInitials
-
# Requires base class to define:
-
# avatar_initials
-
# name
-
# email
-
# deactivated_at
-
-
extend ActiveSupport::Concern
-
-
def set_avatar_initials
-
self.avatar_initials = get_avatar_initials[0..2]
-
end
-
-
def get_avatar_initials
-
if deactivated_at
-
"DU"
-
elsif name.blank? || name == email
-
email.to_s[0..1]
-
else
-
name.split.map(&:first).join
-
end.upcase.gsub(/(\W|\d)/, "")
-
end
-
end
-
1
module DiscussionExportRelations
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
has_many :exportable_polls, -> { where("anonymous = false OR closed_at is not null") }, class_name: 'Poll', foreign_key: :group_id
-
1
has_many :exportable_poll_options, through: :exportable_polls, source: :poll_options
-
1
has_many :exportable_outcomes, through: :exportable_polls, source: :outcomes
-
1
has_many :exportable_stances, through: :exportable_polls, source: :stances
-
1
has_many :exportable_stance_choices, through: :exportable_stances, source: :stance_choices
-
1
has_many :comment_files, through: :comments, source: :files_attachments
-
1
has_many :comment_image_files, through: :comments, source: :image_files_attachments
-
1
has_many :poll_files, through: :exportable_polls, source: :files_attachments
-
1
has_many :poll_image_files, through: :exportable_polls, source: :image_files_attachments
-
1
has_many :outcome_files, through: :exportable_outcomes, source: :files_attachments
-
1
has_many :outcome_image_files, through: :exportable_outcomes, source: :image_files_attachments
-
-
1
has_many :exportable_poll_reactions, -> { joins(:user) }, through: :exportable_polls, source: :reactions
-
1
has_many :exportable_stance_reactions, -> { joins(:user) }, through: :exportable_stances, source: :reactions
-
1
has_many :comment_reactions, -> { joins(:user) }, through: :comments, source: :reactions
-
1
has_many :exportable_outcome_reactions, -> { joins(:user) }, through: :exportable_outcomes, source: :reactions
-
end
-
-
1
def all_reactions
-
Queries::UnionQuery.for(:reactions, [
-
self.reactions,
-
self.exportable_poll_reactions,
-
self.exportable_stance_reactions,
-
self.comment_reactions,
-
self.exportable_outcome_reactions
-
])
-
end
-
end
-
1
module Events::LiveUpdate
-
1
def trigger!
-
850
super
-
850
notify_clients!
-
end
-
-
# send client live updates
-
1
def notify_clients!
-
850
return unless eventable
-
850
if eventable.group_id
-
835
MessageChannelService.publish_models([self], group_id: eventable.group.id)
-
end
-
850
if eventable.respond_to?(:guests)
-
568
eventable.guests.find_each do |user|
-
16
MessageChannelService.publish_models([self], user_id: user.id)
-
end
-
end
-
end
-
end
-
1
module Events::Notify::Author
-
1
def trigger!
-
50
super
-
50
email_author!
-
end
-
-
1
def email_author!
-
50
EventMailer.event(author, self).deliver_later if notify_author?
-
end
-
-
1
private
-
1
def author
-
43
eventable.author
-
end
-
-
# override if we want to send to the author conditionally
-
1
def notify_author?
-
true
-
end
-
end
-
1
module Events::Notify::ByEmail
-
1
def trigger!
-
941
super
-
941
email_users!
-
end
-
-
# send event emails
-
1
def email_users!
-
940
email_recipients.active.uniq.pluck(:id).each do |recipient_id|
-
965
EventMailer.event(recipient_id, self.id).deliver_later
-
end
-
end
-
-
1
def wait_time
-
1.minute
-
end
-
end
-
1
module Events::Notify::Chatbots
-
1
def trigger!
-
901
super
-
901
GenericWorker.set(wait_until: 30.seconds).perform_async('ChatbotService', 'publish_event!', self.id)
-
end
-
end
-
1
module Events::Notify::InApp
-
1
include PrettyUrlHelper
-
-
1
def trigger!
-
792
super
-
792
self.notify_users!
-
end
-
-
# send event notifications
-
1
def notify_users!
-
792
notifications.import(built_notifications)
-
1730
built_notifications.each { |n| MessageChannelService.publish_models(Array(n), user_id: n.user_id) }
-
end
-
-
1
private
-
-
1
def built_notifications
-
2522
@built ||= notification_recipients.active.map { |recipient| notification_for(recipient) }
-
end
-
-
1
def notification_for(recipient)
-
956
I18n.with_locale(recipient.locale) do
-
956
notifications.build(
-
user: recipient,
-
actor: notification_actor,
-
url: notification_url,
-
translation_values: notification_translation_values
-
)
-
end
-
end
-
-
# defines the avatar which appears next to the notification
-
1
def notification_actor
-
1898
user.presence
-
end
-
-
# defines the link that clicking on the notification takes you to
-
1
def notification_url
-
951
polymorphic_path(eventable)
-
end
-
-
# defines the values that are passed to the translation for notification text
-
# by default we infer the values needed from the eventable class,
-
# but this method can be overridden with any translation values for a particular event
-
1
def notification_translation_values
-
{
-
954
name: notification_translation_name,
-
title: notification_translation_title,
-
954
poll_type: (I18n.t(:"poll_types.#{notification_poll_type}") if notification_poll_type)
-
}.compact
-
end
-
-
1
def notification_translation_name
-
954
notification_actor&.name
-
end
-
-
1
def notification_translation_title
-
954
polymorphic_title(eventable)
-
end
-
-
1
def notification_poll_type
-
1843
eventable.poll_type if eventable.respond_to?(:poll_type)
-
end
-
end
-
1
module Events::Notify::Mentions
-
1
def trigger!
-
829
super
-
829
self.notify_mentions!
-
end
-
-
# send event notifications
-
1
def notify_mentions!
-
798
return unless eventable.newly_mentioned_users.any?
-
27
if eventable.respond_to?(:discussion) && eventable.discussion.present?
-
27
eventable.newly_mentioned_users.each do |guest|
-
27
if !eventable.group.members.exists?(guest.id)
-
eventable.discussion.add_guest!(guest, user)
-
end
-
end
-
end
-
27
Events::UserMentioned.publish! eventable, user, eventable.newly_mentioned_users
-
end
-
-
1
private
-
-
# remove newly_mentioned_users from those emailed by following
-
1
def email_recipients
-
573
super.where.not(id: eventable.newly_mentioned_users)
-
end
-
end
-
module GroupExportRelations
-
extend ActiveSupport::Concern
-
-
included do
-
#tags
-
has_many :tags
-
-
# polls
-
has_many :exportable_polls, -> { where("anonymous = false OR closed_at is not null") }, class_name: 'Poll', foreign_key: :group_id
-
-
has_many :discussion_taggings, through: :discussions, source: :taggings
-
has_many :poll_taggings, through: :exportable_polls, source: :taggings
-
has_many :exportable_poll_options, through: :exportable_polls, source: :poll_options
-
has_many :exportable_outcomes, through: :exportable_polls, source: :outcomes
-
has_many :exportable_stances, through: :exportable_polls, source: :stances
-
has_many :exportable_stance_choices, through: :exportable_stances, source: :stance_choices
-
-
# attachments
-
has_many :comment_files, through: :comments, source: :files_attachments
-
has_many :comment_image_files, through: :comments, source: :image_files_attachments
-
has_many :discussion_files, through: :discussions, source: :files_attachments
-
has_many :discussion_image_files, through: :discussions, source: :image_files_attachments
-
has_many :poll_files, through: :exportable_polls, source: :files_attachments
-
has_many :poll_image_files, through: :exportable_polls, source: :image_files_attachments
-
has_many :outcome_files, through: :exportable_outcomes, source: :files_attachments
-
has_many :outcome_image_files, through: :exportable_outcomes, source: :image_files_attachments
-
has_many :subgroup_files, through: :subgroups, source: :files_attachments
-
has_many :subgroup_image_files, through: :subgroups, source: :image_files_attachments
-
has_many :subgroup_cover_photos, through: :subgroups, source: :cover_photo_attachment
-
has_many :subgroup_logos, through: :subgroups, source: :logo_attachment
-
-
# documents
-
has_many :discussion_documents, through: :discussions, source: :documents
-
has_many :exportable_poll_documents, through: :exportable_polls, source: :documents
-
has_many :comment_documents, through: :comments, source: :documents
-
has_many :public_discussion_documents, through: :public_discussions, source: :documents
-
has_many :public_comment_documents, through: :public_comments, source: :documents
-
-
# reactions
-
has_many :discussion_reactions, -> { joins(:user) }, through: :discussions, source: :reactions
-
has_many :exportable_poll_reactions, -> { joins(:user) }, through: :exportable_polls, source: :reactions
-
has_many :exportable_stance_reactions, -> { joins(:user) }, through: :exportable_stances, source: :reactions
-
has_many :comment_reactions, -> { joins(:user) }, through: :comments, source: :reactions
-
has_many :exportable_outcome_reactions, -> { joins(:user) }, through: :exportable_outcomes, source: :reactions
-
-
# readers
-
has_many :discussion_readers, through: :discussions
-
-
# users
-
has_many :discussion_authors, through: :discussions, source: :author
-
has_many :comment_authors, through: :comments, source: :user
-
has_many :exportable_poll_authors, through: :exportable_polls, source: :author
-
has_many :exportable_outcome_authors, through: :exportable_outcomes, source: :author
-
has_many :exportable_stance_authors, through: :exportable_stances, source: :participant
-
has_many :reader_users, through: :discussion_readers, source: :user
-
-
# events
-
has_many :membership_events, through: :memberships, source: :events
-
has_many :discussion_events, through: :discussions, source: :events
-
has_many :comment_events, through: :comments, source: :events
-
has_many :exportable_poll_events, through: :exportable_polls, source: :events
-
has_many :exportable_outcome_events, through: :exportable_outcomes, source: :events
-
has_many :exportable_stance_events, through: :exportable_stances, source: :events
-
end
-
-
def all_users
-
Queries::UnionQuery.for(:users, [
-
self.members,
-
self.discussion_authors,
-
self.comment_authors,
-
self.exportable_poll_authors,
-
self.exportable_outcome_authors,
-
self.exportable_stance_authors,
-
self.reaction_users,
-
self.reader_users
-
])
-
end
-
-
# def related_attachments
-
# Queries::UnionQuery.for(:attachments, [
-
# self.comment_files,
-
# self.comment_image_files,
-
# self.discussion_files,
-
# self.discussion_image_files,
-
# self.poll_files,
-
# self.poll_image_files,
-
# self.outcome_files,
-
# self.outcome_image_files,
-
# self.subgroup_files,
-
# self.subgroup_image_files,
-
# self.subgroup_cover_photos,
-
# self.subgroup_logos,
-
# ])
-
# end
-
-
def all_taggings
-
Queries::UnionQuery.for(:taggings, [
-
self.discussion_taggings,
-
self.poll_taggings
-
])
-
end
-
-
def all_groups
-
Group.where(id: id_and_subgroup_ids)
-
end
-
-
-
def all_events
-
Queries::UnionQuery.for(:events, [
-
self.membership_events,
-
self.discussion_events,
-
self.comment_events,
-
self.exportable_poll_events,
-
self.exportable_outcome_events,
-
self.exportable_stance_events
-
])
-
end
-
-
def all_notifications
-
Notification.where(event_id: all_events.pluck(:id))
-
end
-
-
def all_documents
-
Queries::UnionQuery.for(:documents, [
-
self.documents,
-
self.discussion_documents,
-
self.exportable_poll_documents,
-
self.comment_documents
-
])
-
end
-
-
def all_reactions
-
Queries::UnionQuery.for(:reactions, [
-
self.discussion_reactions,
-
self.exportable_poll_reactions,
-
self.exportable_stance_reactions,
-
self.comment_reactions,
-
self.exportable_outcome_reactions
-
])
-
end
-
-
def reaction_users
-
User.where(id: all_reactions.pluck(:user_id))
-
end
-
end
-
module GroupPrivacy
-
extend ActiveSupport::Concern
-
-
DISCUSSION_PRIVACY_OPTIONS = ['public_only', 'private_only', 'public_or_private'].freeze
-
MEMBERSHIP_GRANTED_UPON_OPTIONS = ['request', 'approval', 'invitation'].freeze
-
-
included do
-
after_initialize :set_privacy_defaults
-
before_validation :set_discussions_private_only, if: :is_hidden_from_public?
-
validate :validate_parent_members_can_see_discussions
-
validate :validate_is_visible_to_parent_members
-
validate :validate_discussion_privacy_options
-
validate :validate_trial_group_cannot_be_public
-
validates_inclusion_of :discussion_privacy_options, in: DISCUSSION_PRIVACY_OPTIONS
-
validates_inclusion_of :membership_granted_upon, in: MEMBERSHIP_GRANTED_UPON_OPTIONS
-
end
-
-
# this method's a bit chunky. New class?
-
def group_privacy=(term)
-
case term
-
when 'open'
-
self.is_visible_to_public = true
-
self.discussion_privacy_options = 'public_only'
-
self.listed_in_explore = true
-
unless %w[approval request invitation].include?(self.membership_granted_upon)
-
self.membership_granted_upon = 'approval'
-
end
-
when 'closed'
-
self.is_visible_to_public = true
-
self.membership_granted_upon = 'approval'
-
self.listed_in_explore = false
-
unless %w[private_only public_or_private].include?(self.discussion_privacy_options)
-
self.discussion_privacy_options = 'private_only'
-
end
-
-
# closed subgroup of hidden parent means parent members can seeee it!
-
if is_subgroup_of_hidden_parent?
-
self.is_visible_to_parent_members = true
-
self.is_visible_to_public = false
-
end
-
when 'secret'
-
self.is_visible_to_public = false
-
self.listed_in_explore = false
-
self.discussion_privacy_options = 'private_only'
-
self.membership_granted_upon = 'invitation'
-
self.is_visible_to_parent_members = false
-
else
-
raise "group_privacy term not recognised: #{term}"
-
end
-
end
-
-
def group_privacy
-
if is_visible_to_public?
-
self.public_discussions_only? ? 'open' : 'closed'
-
elsif parent_id && is_visible_to_parent_members?
-
'closed'
-
else
-
'secret'
-
end
-
end
-
-
def is_hidden_from_public?
-
!is_visible_to_public?
-
end
-
-
def private_discussions_only?
-
discussion_privacy_options == 'private_only'
-
end
-
-
def public_discussions_only?
-
discussion_privacy_options == 'public_only'
-
end
-
-
def public_or_private_discussions_allowed?
-
discussion_privacy_options == 'public_or_private'
-
end
-
-
def membership_granted_upon_approval?
-
membership_granted_upon == 'approval'
-
end
-
-
def membership_granted_upon_request?
-
membership_granted_upon == 'request'
-
end
-
-
def membership_granted_upon_invitation?
-
membership_granted_upon == 'invitation'
-
end
-
-
def discussion_private_default
-
self.discussion_privacy_options != "public_only"
-
end
-
-
def set_discussions_private_only
-
self.discussion_privacy_options = 'private_only'
-
end
-
-
def validate_discussion_privacy_options
-
unless is_visible_to_parent_members?
-
if group_privacy == 'open' and !public_discussions_only?
-
self.errors.add(:discussion_privacy_options, "Discussions must be public if group is open")
-
end
-
end
-
-
if is_hidden_from_public? and not private_discussions_only?
-
self.errors.add(:discussion_privacy_options, "Discussions must be private if group is hidden")
-
end
-
end
-
-
def validate_parent_members_can_see_discussions
-
self.errors.add(:parent_members_can_see_discussions) unless parent_members_can_see_discussions_is_valid?
-
end
-
-
def validate_is_visible_to_parent_members
-
self.errors.add(:is_visible_to_parent_members) unless visible_to_parent_members_is_valid?
-
end
-
-
def validate_trial_group_cannot_be_public
-
if !self.parent_id &&
-
self.subscription &&
-
self.subscription.plan == 'trial' &&
-
self.is_visible_to_public
-
self.errors.add(:group_privacy, I18n.t('group.error.no_public_trials'))
-
end
-
end
-
-
def parent_members_can_see_discussions_is_valid?
-
if is_visible_to_public?
-
true
-
else
-
if parent_members_can_see_discussions?
-
is_visible_to_parent_members?
-
else
-
true
-
end
-
end
-
end
-
-
def visible_to_parent_members_is_valid?
-
if is_visible_to_public?
-
true
-
else
-
if is_visible_to_parent_members?
-
is_hidden_from_public? and is_subgroup?
-
else
-
true
-
end
-
end
-
end
-
-
def set_privacy_defaults
-
self.is_visible_to_public ||= false
-
self.discussion_privacy_options ||= 'private_only'
-
self.membership_granted_upon ||= 'approval'
-
end
-
-
end
-
module HasAvatar
-
include AvatarInitials
-
include Routing
-
extend ActiveSupport::Concern
-
-
included do
-
include Gravtastic
-
gravtastic rating: :pg, default: :none
-
before_create :set_default_avatar_kind
-
end
-
-
def set_default_avatar_kind
-
if uploaded_avatar.attached?
-
self.avatar_kind = :uploaded
-
elsif has_gravatar?
-
self.avatar_kind = :gravatar
-
else
-
self.avatar_kind = :initials
-
end
-
end
-
-
def avatar_kind
-
return 'mdi-duck' if deactivated_at?
-
return 'mdi-email-outline' if !name
-
super
-
end
-
-
def thumb_url
-
avatar_url(128)
-
end
-
-
def avatar_url(size = 512)
-
if avatar_kind == 'uploaded' && (!uploaded_avatar.attached? or uploaded_avatar.attachment.nil?)
-
update_columns(avatar_kind: set_default_avatar_kind)
-
end
-
-
case avatar_kind
-
when 'gravatar'
-
gravatar_url(size: size, secure: true, default: 'retro')
-
when 'uploaded'
-
uploaded_avatar_url(size)
-
else
-
nil
-
end
-
rescue ActiveStorage::UnrepresentableError
-
update_columns(avatar_kind: :initials)
-
nil
-
end
-
-
def avatar_initials_url(size = 256)
-
colors = AppConfig.theme[:brand_colors].slice(:gold, :sky, :wellington, :sunset).values
-
color = colors[id % colors.length].gsub('#','')
-
params = {
-
name: String(avatar_initials).split('').join('+'),
-
background: colors[id % colors.length].gsub('#',''),
-
color: '000000',
-
rounded: true,
-
format: :png,
-
size: size
-
}
-
"https://ui-avatars.com/api/?#{params.to_a.map{|p| p.join('=')}.join('&')}"
-
end
-
-
def uploaded_avatar_url(size = 512)
-
size = size.to_i
-
return unless uploaded_avatar.attached?
-
Rails.application.routes.url_helpers.rails_representation_path(
-
uploaded_avatar.representation(resize_to_limit: [size,size], saver: {quality: 80, strip: true}),
-
only_path: true
-
)
-
end
-
-
def has_gravatar?(options = {})
-
return false if Rails.env.test?
-
hash = Digest::MD5.hexdigest(email.to_s.downcase)
-
options = { :rating => 'x', :timeout => 2 }.merge(options)
-
http = Net::HTTP.new('www.gravatar.com', 80)
-
http.read_timeout = options[:timeout]
-
response = http.request_head("/avatar/#{hash}?rating=#{options[:rating]}&default=http://gravatar.com/avatar")
-
response.code != '302'
-
rescue StandardError, Timeout::Error
-
false # Don't show "gravatar" if the service is down or slow
-
end
-
-
end
-
module HasCreatedEvent
-
def created_event
-
events.find_by(kind: created_event_kind)
-
end
-
-
def created_event_kind
-
:"#{self.class.name.downcase}_created"
-
end
-
-
def create_missing_created_event!
-
self.events.create(kind: created_event_kind, user_id: author_id, created_at: created_at)
-
end
-
end
-
module HasCustomFields
-
def set_custom_fields(*fields)
-
fields.map(&:to_s).each do |field|
-
define_method field, -> { self[:custom_fields][field] }
-
define_method :"#{field}=", ->(value) { self[:custom_fields][field] = value }
-
end
-
end
-
end
-
module HasDefaults
-
def initialized_with_default(column, method = nil)
-
after_initialize do
-
send(:"#{column}=", method&.call) if send(column) == nil
-
end
-
end
-
end
-
module HasEvents
-
extend ActiveSupport::Concern
-
-
included do
-
has_many :events, -> { includes :user, :eventable }, as: :eventable, dependent: :destroy
-
has_many :notifications, through: :events
-
has_many :users_notified, -> { distinct }, through: :notifications, source: :user
-
end
-
end
-
module HasExperiences
-
def experienced!(key, toggle = true)
-
experiences[key] = toggle
-
save
-
end
-
end
-
module HasMentions
-
extend ActiveSupport::Concern
-
include Twitter::Extractor
-
include HasEvents
-
-
module ClassMethods
-
def is_mentionable(on: [])
-
define_singleton_method :mentionable_fields, -> { Array on }
-
end
-
end
-
-
def mentioned_usernames
-
if text_format == "md"
-
extract_mentioned_screen_names(mentionable_text).uniq - [self.author&.username]
-
else
-
Nokogiri::HTML::fragment(mentionable_text).search("span[data-mention-id]").map do |el|
-
el['data-mention-id']
-
end.filter { |id_or_username| id_or_username.to_i.to_s != id_or_username }
-
end
-
end
-
-
def mentioned_user_ids
-
# html text could use ids or usernames depending on the age of the content
-
return [] if text_format == "md"
-
Nokogiri::HTML::fragment(mentionable_text).search("span[data-mention-id]").map do |el|
-
el['data-mention-id']
-
end.filter { |id_or_username| id_or_username.to_i.to_s == id_or_username }
-
end
-
-
def mentioned_users
-
members.where("users.username in (:usernames) or users.id in (:ids)", usernames: mentioned_usernames, ids: mentioned_user_ids)
-
end
-
-
# users mentioned in the text, but not yet sent notifications
-
def newly_mentioned_users
-
mentioned_users
-
.where.not(id: already_mentioned_users) # avoid re-mentioning users when editing
-
.where.not(id: users_to_not_mention)
-
end
-
-
# users mentioned on a previous edit of this model
-
def already_mentioned_users
-
User.where(id: self.notifications.user_mentions.pluck(:user_id))
-
end
-
-
def users_to_not_mention
-
User.none # overridden with specific users to not receive mentions
-
end
-
-
private
-
-
def text_format
-
self.send("#{self.class.mentionable_fields.first}_format")
-
end
-
-
def mentionable_text
-
self.class.mentionable_fields.map { |field| self.send(field) }.join('|')
-
end
-
end
-
module HasRichText
-
PREVIEW_OPTIONS = {
-
resize_to_limit: [1280,1280],
-
saver: {
-
quality: 85,
-
strip: true
-
}
-
}
-
-
extend ActiveSupport::Concern
-
-
module ClassMethods
-
def is_rich_text(on: [])
-
define_singleton_method :rich_text_fields, -> { Array on }
-
rich_text_fields.each do |field|
-
define_method "sanitize_#{field}!" do
-
# return if self.send("#{field}_format") == 'md'
-
tags = %w[strong em b i p s code pre big div small hr br span mark h1 h2 h3 ul ol li abbr a img video audio blockquote table thead th tr td iframe u]
-
attributes = %w[href src alt title data-type data-iframe-container data-done data-mention-id poster controls data-author-id data-uid data-checked data-due-on data-color data-remind width height target colspan rowspan data-text-align]
-
-
self[field] = Rails::Html::WhiteListSanitizer.new.sanitize(self[field], tags: tags, attributes: attributes)
-
self[field] = HasRichText::strip_empty_paragraphs(self[field])
-
self[field] = add_required_link_attributes(self[field])
-
self[field] = HasRichText::add_heading_ids(self[field])
-
self[field] = TaskService.rewrite_uids(self[field])
-
# preserve markdown quotes after html sanitizer
-
self[field] = self[field].gsub(/^>\; /, '> ') if self.send("#{field}_format") == 'md'
-
end
-
-
define_method "#{field}_visible_text" do
-
if self.send("#{field}_format") == 'html'
-
Nokogiri::HTML(self[field]).text
-
else
-
Nokogiri::HTML(MarkdownService.render_html(self[field])).text
-
end
-
end
-
-
define_method "body_is_blank?" do
-
self[field] == '' ||
-
self[field] == nil ||
-
self[field] == '<p></p>'
-
end
-
-
before_save :"sanitize_#{field}!"
-
-
define_method "parse_and_update_tasks_#{field}!" do
-
TaskService.parse_and_update(self, field)
-
end
-
after_save :"parse_and_update_tasks_#{field}!"
-
-
validates field, {length: {maximum: Rails.application.secrets.max_message_length}}
-
validates_inclusion_of :"#{field}_format", in: ['html', 'md']
-
if respond_to?(:after_discard)
-
after_discard do
-
tasks.discard_all
-
end
-
after_undiscard do
-
tasks.undiscard_all
-
end
-
end
-
end
-
end
-
end
-
-
included do
-
has_many_attached :files, dependent: :detach
-
has_many_attached :image_files, dependent: :detach
-
has_many :tasks, as: :record
-
before_save :update_content_locale
-
before_save :build_attachments
-
before_save :sanitize_link_previews
-
end
-
-
def update_content_locale
-
return unless self.changed.intersection(self.class.rich_text_fields.map(&:to_s)).any?
-
-
combined_text = self.class.rich_text_fields.map {|field| self[field] }.join(' ')
-
stripped_text = Rails::Html::WhiteListSanitizer.new.sanitize(combined_text, tags: [])
-
result = CLD.detect_language stripped_text
-
self.content_locale = result[:code] if result[:reliable]
-
end
-
-
def sanitize_link_previews
-
sanitizer = Rails::Html::FullSanitizer.new
-
self.link_previews.each do |preview|
-
preview.keys.each do |key|
-
preview[key] = String(sanitizer.sanitize preview[key]).truncate(480)
-
end
-
end
-
end
-
-
def build_attachments
-
# this line is just to help migrations through
-
return true unless self.class.column_names.include?('attachments')
-
self[:attachments] = files.map do |file|
-
i = file.blob.slice(:id, :filename, :content_type, :byte_size)
-
i.merge!({ preview_url: Rails.application.routes.url_helpers.rails_representation_path(file.representation(PREVIEW_OPTIONS), only_path: true) }) if file.representable?
-
i.merge!({ download_url: Rails.application.routes.url_helpers.rails_blob_path(file, only_path: true) })
-
i.merge!({ icon: attachment_icon(file.content_type || file.filename) })
-
i.merge!({ signed_id: file.signed_id })
-
i
-
end
-
end
-
-
def assign_attributes_and_files(params)
-
self.assign_attributes API::V1::SnorlaxBase.filter_params(self.class, params)
-
end
-
-
def attachment_icon(name)
-
AppConfig.doctypes.detect{ |type| /#{type['regex']}/.match(name) }['icon']
-
end
-
-
def self.strip_empty_paragraphs(text)
-
fragment = Nokogiri::HTML::DocumentFragment.parse(text)
-
fragment.css('p').each do |node|
-
if node.content.match?(/^[[:space:]]+$/)
-
node.content = node.content.gsub(/^[[:space:]]+$/, '')
-
end
-
end
-
fragment.to_s
-
end
-
-
def self.add_heading_ids(text)
-
fragment = Nokogiri::HTML::DocumentFragment.parse(text)
-
fragment.css('h1,h2,h3,h4,h5,h6').each do |node|
-
node['id'] = node.text[0,60].strip.parameterize
-
end
-
fragment.to_s
-
end
-
-
def add_required_link_attributes(text)
-
fragment = Nokogiri::HTML::DocumentFragment.parse(text)
-
fragment.css('a').each do |node|
-
node['rel'] = 'nofollow ugc noreferrer noopener'
-
node['target'] = '_blank'
-
end
-
fragment.to_s
-
end
-
end
-
module HasTags
-
extend ActiveSupport::Concern
-
-
included do
-
after_save :update_group_tags
-
end
-
-
def tag_models
-
group.tags.where(name: self.tags).order(:priority)
-
end
-
-
def update_group_tags
-
return unless self.group_id
-
GenericWorker.perform_async('TagService', 'update_group_and_org_tags', self.group_id)
-
end
-
end
-
module HasTimeframe
-
extend ActiveSupport::Concern
-
-
included do
-
scope :within, ->(since, till, field = nil) { where("#{self.table_name}.#{field || :created_at} BETWEEN ? AND ?", since || 100.years.ago, till || 100.years.from_now) }
-
scope :until, ->(till) { within(nil, till) }
-
scope :since, ->(since) { within(since, nil) }
-
-
def self.has_timeframe?
-
true
-
end
-
-
end
-
-
end
-
module HasTokens
-
def initialized_with_token(column, method = nil)
-
after_initialize do
-
send(:"#{column}=", send(column) || method&.call || self.class.generate_unique_secure_token) if self.respond_to?("#{column}=")
-
end
-
end
-
end
-
module HasVolume
-
extend ActiveSupport::Concern
-
-
included do
-
enum volume: {mute: 0, quiet: 1, normal: 2, loud: 3}
-
scope :volume, ->(volume) { where(volume: volumes[volume]) }
-
scope :volume_at_least, ->(volume) { where('volume >= ?', volumes[volume]) }
-
scope :email_notifications, -> { where('volume >= ?', volumes[:normal]) }
-
scope :app_notifications, -> { where('volume >= ?', volumes[:quiet]) }
-
end
-
-
def set_volume!(volume, persist: true)
-
if self.class.volumes.include?(volume)
-
self.volume = volume
-
save if persist
-
else
-
self.errors.add :volume, I18n.t(:"activerecord.errors.messages.invalid")
-
false
-
end
-
end
-
-
def volume_is_normal_or_loud?
-
volume_is_normal? || volume_is_loud?
-
end
-
-
def volume_is_loud?
-
volume.to_s == 'loud'
-
end
-
-
def volume_is_normal?
-
volume.to_s == 'normal'
-
end
-
-
def volume_is_quiet?
-
volume.to_s == 'quiet'
-
end
-
-
def volume_is_mute?
-
volume.to_s == 'mute'
-
end
-
end
-
module Identities::WithClient
-
def notify!(event)
-
return unless valid_event_kinds.include?(event.kind)
-
I18n.with_locale(event.group.locale) { client.post_content!(event) }
-
end
-
-
def fetch_user_info
-
apply_user_info(client.fetch_user_info.json)
-
end
-
-
private
-
-
def valid_event_kinds
-
[
-
'new_discussion',
-
'poll_created',
-
'poll_closing_soon',
-
'poll_expired',
-
'outcome_created'
-
]
-
end
-
-
def client
-
@client ||= "Clients::#{identity_type.to_s.classify}".constantize.new(token: self.access_token)
-
end
-
-
# called by default immediately after an access token is obtained.
-
# Define a method here to get some basic information about the user,
-
# like name, email, profile image, etc
-
def apply_user_info(payload)
-
raise NotImplementedError.new
-
end
-
end
-
module MessageChannel
-
extend ActiveSupport::Concern
-
-
def message_channel
-
"/#{self.class.to_s.downcase}-#{self.key}"
-
end
-
end
-
module NoForbiddenEmails
-
extend ActiveSupport::Concern
-
FORBIDDEN_EMAIL_ADDRESSES = [ENV.fetch('DECIDE_EMAIL', "decide@#{ENV['CANONICAL_HOST']}")]
-
-
included do
-
validates_exclusion_of :email, in: FORBIDDEN_EMAIL_ADDRESSES
-
end
-
end
-
module NoSpam
-
# eg:
-
# SPAM_REGEX="(diide\.com|gusronk\.com|appnox\.com|akxpert\.com)"
-
SPAM_REGEX = Regexp.new(ENV.fetch('SPAM_REGEX', "(diide\.com|gusronk\.com)"), 'i')
-
-
def no_spam_for(*fields)
-
Array(fields).each do |field|
-
validates field, format: { without: SPAM_REGEX, message: "no spam" }
-
end
-
end
-
end
-
1
module Null::Group
-
1
include Null::Object
-
-
1
def group
-
self
-
end
-
-
1
alias :read_attribute_for_serialization :send
-
-
1
def new_threads_max_depth
-
2
-
end
-
-
1
def new_threads_newest_first
-
false
-
end
-
-
1
def full_name
-
I18n.t('discussion.direct_thread')
-
end
-
-
1
def save
-
4
true
-
end
-
-
1
def name
-
I18n.t('discussion.direct_thread')
-
end
-
-
1
def nil_methods
-
%w(
-
2795
parent
-
id
-
key
-
locale
-
update_polls_count
-
update_closed_polls_count
-
update_discussions_count
-
update_public_discussions_count
-
update_open_discussions_count
-
update_closed_discussions_count
-
update_discussion_templates_count
-
presence
-
present?
-
content_locale
-
handle
-
description
-
description_format
-
group_id
-
add_member!
-
message_channel
-
logo_or_parent_logo
-
created_at
-
creator_id
-
cover_url
-
logo_url
-
category
-
)
-
end
-
-
1
def true_methods
-
%w[
-
2795
private_discussions_only?
-
members_can_raise_motions
-
members_can_edit_comments
-
members_can_delete_comments
-
discussion_private_default
-
members_can_announce
-
members_can_edit_discussions
-
members_can_add_guests
-
]
-
end
-
-
1
def empty_methods
-
%w[
-
2795
member_ids
-
identities
-
hidden_poll_templates
-
hidden_discussion_templates
-
]
-
end
-
-
1
def discussion_privacy_options
-
'private_only'
-
end
-
-
1
def false_methods
-
%w(
-
2795
public_discussions_only?
-
is_visible_to_parent_members
-
members_can_add_members
-
members_can_add_guests
-
members_can_create_subgroups
-
members_can_edit_discussions
-
members_can_start_discussions
-
admins_can_edit_user_content
-
)
-
end
-
-
1
def zero_methods
-
%w[
-
2795
memberships_count
-
polls_count
-
closed_polls_count
-
discussions_count
-
public_discussions_count
-
pending_memberships_count
-
discussion_templates_count
-
]
-
end
-
-
1
def none_methods
-
2795
{
-
members: :user,
-
self_and_subgroups: :group,
-
accepted_members: :user,
-
chatbots: :chatbot,
-
tags: :tag,
-
poll_templates: :poll_template,
-
discussion_templates: :discussion_template,
-
memberships: :membership,
-
admins: :user,
-
webhooks: :webhook,
-
}
-
end
-
-
1
def discussion_templates=(arg)
-
nil
-
end
-
-
1
def group_privacy
-
'private_only'
-
end
-
-
1
def parent_or_self
-
self
-
end
-
-
1
def self_or_parent_logo_url(size)
-
nil
-
end
-
-
1
def self_or_parent_cover_url(size)
-
nil
-
end
-
-
1
def id_and_subgroup_ids
-
[]
-
end
-
-
1
def poll_template_positions
-
{
-
'question' => 0,
-
'check' => 1,
-
'advice' => 2,
-
'consent' => 3,
-
'consensus' => 4,
-
'gradients_of_agreement' => 5,
-
'poll' => 6,
-
'score' => 7,
-
'dot_vote' => 8,
-
'ranked_choice' => 9,
-
'meeting' => 10,
-
'count' => 11,
-
}
-
end
-
-
1
def discussion_template_positions
-
{
-
'blank' => 0,
-
'open_discussion' => 1,
-
'updates_thread' => 2,
-
}
-
end
-
-
1
def subscription
-
{
-
max_members: nil,
-
max_threads: nil,
-
active: true,
-
members_count: 0
-
}
-
end
-
end
-
1
module Null::Object
-
1
def apply_null_methods!
-
6100
apply_null_method :nil_methods, nil
-
6100
apply_null_method :false_methods, false
-
6100
apply_null_method :empty_methods, []
-
6100
apply_null_method :hash_methods, {}
-
6100
apply_null_method :true_methods, true
-
6100
apply_null_method :zero_methods, 0
-
6118
apply_null_method :none_methods, ->(model) { model.to_s.singularize.classify.constantize.none }
-
end
-
-
1
def apply_null_method(name, value)
-
42700
send(name).each do |method, model|
-
380995
self.class.send :define_method, method, ->(*args) {
-
5518
value.respond_to?(:call) ? value.call(model) : value
-
}
-
end
-
end
-
-
1
def blank?
-
438
true
-
end
-
-
1
def present?
-
9
false
-
end
-
-
1
def presence
-
nil
-
end
-
-
1
def marked_for_destruction?
-
90
false
-
end
-
-
1
def nil_methods
-
[]
-
end
-
-
1
def false_methods
-
[]
-
end
-
-
1
def empty_methods
-
[]
-
end
-
-
1
def hash_methods
-
2795
[]
-
end
-
-
1
def true_methods
-
3305
[]
-
end
-
-
1
def zero_methods
-
3305
[]
-
end
-
-
1
def none_methods
-
[]
-
end
-
end
-
1
module Null::User
-
# include HasAvatar
-
1
include Null::Object
-
1
def can?(*args)
-
3
ability.can?(*args)
-
end
-
-
1
def ability
-
26
@ability ||= Ability::Base.new(self)
-
end
-
-
1
def nil_methods
-
3305
[:id, :key, :username, :short_bio, :city, :region, :country, :selected_locale, :deactivated_at,
-
:default_membership_volume, :unsubscribe_token, :location, :email_catch_up_day,
-
:encrypted_password, :update_attribute, :last_seen_at, :legal_accepted_at, :api_key]
-
end
-
-
1
def false_methods
-
3305
[:is_logged_in?, :is_member_of?, :is_admin_of?, :is_admin?, :is_admin, :api_key_changed?,
-
:email_when_proposal_closing_soon, :has_password, :bot, :bot?,
-
:email_when_mentioned, :email_on_participation, :email_verified, :email_verified?, :email_newsletter, :marked_for_destruction?]
-
end
-
-
1
def empty_methods
-
3305
[:group_ids, :adminable_group_ids, :group_ids, :attachments, :guest_discussion_ids]
-
end
-
-
1
def hash_methods
-
3305
[:experiences]
-
end
-
-
1
def none_methods
-
3305
{
-
notifications: :notification,
-
login_tokens: :login_token,
-
memberships: :membership,
-
admin_memberships: :membership,
-
participated_polls: :poll,
-
group_polls: :poll,
-
polls: :poll,
-
stances: :stance,
-
groups: :group,
-
adminable_groups: :group
-
}
-
end
-
-
1
def avatar_initials_url(size = 256)
-
params = {
-
409
name: "AU".split('').join('+'),
-
background: AppConfig.theme[:brand_colors][:gold].gsub('#',''),
-
color: '000000',
-
rounded: true,
-
format: :png,
-
size: size
-
}
-
2863
"https://ui-avatars.com/api/?#{params.to_a.map{|p| p.join('=')}.join('&')}"
-
end
-
-
1
def default_format
-
"html"
-
end
-
-
1
def short_bio_format
-
"html"
-
end
-
-
1
def identities
-
Identities::Base.none
-
end
-
-
1
def is_admin?
-
false
-
end
-
end
-
module Reactable
-
def self.included(base)
-
base.has_many :reactions, -> { joins(:user).where("users.deactivated_at": nil) }, dependent: :destroy, as: :reactable
-
base.has_many :reactors, through: :reactions, source: :user
-
end
-
end
-
module ReadableUnguessableUrls
-
extend ActiveSupport::Concern
-
-
KEY_LENGTH = 8
-
-
included do |base|
-
base.extend FriendlyId
-
base.send :friendly_id, :key, use: [:finders]
-
base.send :before_validation, :set_key
-
base.send :validates, :key, presence: true
-
end
-
-
def set_key
-
if self.key.blank?
-
self.key = generate_unique_key
-
end
-
end
-
-
private
-
def generate_unique_key
-
begin
-
key = generate_key
-
end while self.class.default_scoped.where(key: key).exists? or key.match(/^\d+$/)
-
key
-
end
-
-
def generate_key
-
(('a'..'z').to_a +
-
('A'..'Z').to_a +
-
(0..9).to_a ).sample(KEY_LENGTH).join
-
end
-
end
-
module Routing
-
extend ActiveSupport::Concern
-
include Rails.application.routes.url_helpers
-
-
included do
-
def default_url_options
-
ActionMailer::Base.default_url_options
-
end
-
end
-
end
-
module Searchable
-
extend ActiveSupport::Concern
-
include PgSearch::Model
-
-
included do
-
multisearchable
-
end
-
-
module ClassMethods
-
def rebuild_pg_search_documents
-
connection.execute pg_search_insert_statement
-
end
-
-
def pg_search_insert_statement(id: nil, author_id: nil, discussion_id: nil)
-
raise "expected to be overwritten"
-
end
-
end
-
end
-
-
module PgSearch::Multisearchable
-
def update_pg_search_document
-
PgSearch::Document.where(searchable: self).delete_all
-
ActiveRecord::Base.connection.execute(self.class.pg_search_insert_statement(id: self.id))
-
end
-
end
-
module SelfReferencing
-
extend ActiveSupport::Concern
-
-
included do |base|
-
define_method base.name.downcase, -> { self }
-
define_method :"#{base.name.downcase}_id", -> { self.id }
-
end
-
end
-
module Translatable
-
extend ActiveSupport::Concern
-
-
included do
-
has_many :translations, as: :translatable
-
before_update :clear_translations, if: :translatable_fields_modified?
-
end
-
-
def translatable_fields_modified?
-
return unless TranslationService.available?
-
(self.saved_changes.keys.map(&:to_sym) & self.class.translatable_fields).any?
-
end
-
-
def clear_translations
-
self.translations.delete_all
-
end
-
-
module ClassMethods
-
def is_translatable(on: [], load_via: :find, id_field: :id, locale_field: :locale)
-
-
define_singleton_method :translatable_fields, -> { Array on }
-
define_singleton_method :get_instance, ->(id) { send load_via, id }
-
-
define_method :id_field, -> { send id_field }
-
define_method :locale_field, -> { send locale_field }
-
end
-
end
-
end
-
module UsesOrganisationScope
-
extend ActiveSupport::Concern
-
-
included do
-
scope :in_organisation, -> (group) { where(group_id: group.id_and_subgroup_ids) }
-
end
-
end
-
class ContactMessage
-
include ActiveModel::Model
-
include ActiveModel::Validations
-
-
alias :read_attribute_for_serialization :send
-
attr_accessor :name, :email, :user_id, :subject, :message
-
-
validates :email, presence: true, email: true
-
# validates :message, presence: true, length: { maximum: Rails.application.secrets.max_message_length }
-
end
-
class Demo < ApplicationRecord
-
belongs_to :author, class_name: 'User'
-
belongs_to :group
-
validates :name, presence: true
-
-
def self.ransackable_attributes(auth_object = nil)
-
["author_id", "created_at", "demo_handle", "description", "group_id", "id", "name", "priority", "recorded_at", "updated_at"]
-
end
-
end
-
1
class Discussion < ApplicationRecord
-
1
include CustomCounterCache::Model
-
1
include ReadableUnguessableUrls
-
1
include Translatable
-
1
include Reactable
-
1
include HasTimeframe
-
1
include HasEvents
-
1
include HasMentions
-
1
include MessageChannel
-
1
include SelfReferencing
-
1
include HasCreatedEvent
-
1
include HasRichText
-
1
include HasTags
-
1
include Discard::Model
-
-
1
include Searchable
-
-
1
def self.pg_search_insert_statement(id: nil, author_id: nil)
-
4971
content_str = "regexp_replace(CONCAT_WS(' ', discussions.title, discussions.description, users.name), E'<[^>]+>', '', 'gi')"
-
4971
<<~SQL.squish
-
INSERT INTO pg_search_documents (
-
searchable_type,
-
searchable_id,
-
group_id,
-
discussion_id,
-
author_id,
-
authored_at,
-
content,
-
ts_content,
-
created_at,
-
updated_at)
-
SELECT 'Discussion' AS searchable_type,
-
discussions.id AS searchable_id,
-
discussions.group_id as group_id,
-
discussions.id AS discussion_id,
-
discussions.author_id AS author_id,
-
discussions.created_at AS authored_at,
-
#{content_str} AS content,
-
to_tsvector('simple', #{content_str}) as ts_content,
-
now() AS created_at,
-
now() AS updated_at
-
FROM discussions
-
LEFT JOIN users ON users.id = discussions.author_id
-
WHERE discarded_at IS NULL
-
#{id ? " AND discussions.id = #{id.to_i} LIMIT 1" : ""}
-
#{author_id ? " AND discussions.author_id = #{author_id.to_i}" : ""}
-
SQL
-
end
-
-
1
scope :dangling, -> { joins('left join groups g on discussions.group_id = g.id').where('group_id is not null and g.id is null') }
-
1
scope :in_organisation, -> (group) { includes(:author).where(group_id: group.id_and_subgroup_ids) }
-
6
scope :last_activity_after, -> (time) { where('last_activity_at > ?', time) }
-
16
scope :order_by_latest_activity, -> { order(last_activity_at: :desc) }
-
5
scope :recent, -> { where('last_activity_at > ?', 6.weeks.ago) }
-
-
4583
scope :visible_to_public, -> { kept.where(private: false) }
-
1
scope :not_visible_to_public, -> { kept.where(private: true) }
-
-
4584
scope :is_open, -> { kept.where(closed_at: nil) }
-
4573
scope :is_closed, -> { kept.where("closed_at is not null") }
-
-
1
validates_presence_of :title, :group, :author
-
1
validates :title, length: { maximum: 150 }
-
1
validates :description, length: { maximum: Rails.application.secrets.max_message_length }
-
1
validate :privacy_is_permitted_by_group
-
-
1
is_mentionable on: :description
-
1
is_translatable on: [:title, :description], load_via: :find_by_key!, id_field: :key
-
1
is_rich_text on: :description
-
1
has_paper_trail only: [:title, :description, :description_format, :private, :group_id, :author_id, :tags, :closed_at, :closer_id]
-
-
1
belongs_to :group, class_name: 'Group'
-
1
belongs_to :author, class_name: 'User'
-
1
belongs_to :user, foreign_key: 'author_id'
-
1
belongs_to :closer, foreign_key: 'closer_id', class_name: "User"
-
1
has_many :polls, dependent: :destroy
-
1
has_many :active_polls, -> { where(closed_at: nil) }, class_name: "Poll"
-
-
1
has_many :comments, dependent: :destroy
-
1
has_many :commenters, -> { uniq }, through: :comments, source: :user
-
1
has_many :documents, as: :model, dependent: :destroy
-
1
has_many :poll_documents, through: :polls, source: :documents
-
1
has_many :comment_documents, through: :comments, source: :documents
-
-
2409
has_many :items, -> { includes(:user) }, class_name: 'Event', dependent: :destroy
-
-
1
has_many :discussion_readers, dependent: :destroy
-
11
has_many :readers,-> { merge DiscussionReader.active }, through: :discussion_readers, source: :user
-
1
has_many :guests, -> { merge DiscussionReader.guests }, through: :discussion_readers, source: :user
-
1
has_many :admin_guests, -> { merge DiscussionReader.admins }, through: :discussion_readers, source: :user
-
1
include DiscussionExportRelations
-
-
1
scope :search_for, -> (q) do
-
kept.where("discussions.title ilike ?", "%#{q}%")
-
end
-
-
1
delegate :name, to: :group, prefix: :group
-
1
delegate :name, to: :author, prefix: :author
-
1
delegate :users, to: :group, prefix: :group
-
1
delegate :full_name, to: :group, prefix: :group
-
1
delegate :email, to: :author, prefix: :author
-
1
delegate :name_and_email, to: :author, prefix: :author
-
1
delegate :locale, to: :author
-
-
1
after_create :set_last_activity_at_to_created_at
-
1
after_destroy :drop_sequence_id_sequence
-
-
502
define_counter_cache(:closed_polls_count) { |d| d.polls.closed.count }
-
11
define_counter_cache(:versions_count) { |d| d.versions.count }
-
1165
define_counter_cache(:seen_by_count) { |d| d.discussion_readers.where("last_read_at is not null").count }
-
1575
define_counter_cache(:members_count) { |d| d.discussion_readers.where("revoked_at is null").count }
-
502
define_counter_cache(:anonymous_polls_count) { |d| d.polls.where(anonymous: true).count }
-
-
1
update_counter_cache :group, :discussions_count
-
1
update_counter_cache :group, :public_discussions_count
-
1
update_counter_cache :group, :open_discussions_count
-
1
update_counter_cache :group, :closed_discussions_count
-
1
update_counter_cache :group, :closed_polls_count
-
-
1
def poll
-
nil
-
end
-
-
1
def group
-
42167
super || NullGroup.new
-
end
-
-
1
def existing_member_ids
-
reader_ids
-
end
-
-
1
def user_id
-
1336
author_id
-
end
-
-
1
def author
-
8856
super || AnonymousUser.new
-
end
-
-
1
def members
-
4549
User.active.
-
joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{self.id || 0} AND dr.user_id = users.id").
-
joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}").
-
where('(m.id IS NOT NULL AND m.revoked_at IS NULL) OR
-
(dr.id IS NOT NULL AND dr.guest = TRUE AND dr.revoked_at IS NULL)')
-
end
-
-
1
def admins
-
86
User.active.
-
joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{self.id || 0} AND dr.user_id = users.id").
-
joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}").
-
where('(m.admin = TRUE AND m.id IS NOT NULL AND m.revoked_at IS NULL) OR
-
(dr.admin = TRUE AND dr.id IS NOT NULL AND dr.revoked_at IS NULL)')
-
end
-
-
1
def guests
-
577
members.where('m.group_id is null')
-
end
-
-
1
def add_guest!(user, inviter)
-
64
if dr = discussion_readers.find_by(user: user)
-
1
dr.update(guest: true, inviter: inviter)
-
else
-
63
discussion_readers.create!(user: user, inviter: inviter, guest: true, volume: DiscussionReader.volumes[:normal])
-
end
-
end
-
-
1
def add_admin!(user, inviter)
-
if dr = discussion_readers.find_by(user: user)
-
dr.update(inviter: inviter, admin: true)
-
else
-
discussion_readers.create!(user: user, inviter: inviter, admin: true, volume: DiscussionReader.volumes[:normal])
-
end
-
end
-
-
1
def poll_id
-
nil
-
end
-
-
1
def created_event_kind
-
1617
:new_discussion
-
end
-
-
1
def update_sequence_info!
-
1267
sequence_ids = discussion.items.order(:sequence_id).pluck(:sequence_id).compact
-
1267
discussion.ranges_string = RangeSet.serialize RangeSet.reduce RangeSet.ranges_from_list sequence_ids
-
1267
discussion.last_activity_at = discussion.items.unreadable.order(:sequence_id).last&.created_at || created_at
-
1267
update_columns(
-
items_count: sequence_ids.count,
-
ranges_string: discussion.ranges_string,
-
last_activity_at: discussion.last_activity_at)
-
end
-
-
1
def drop_sequence_id_sequence
-
10
SequenceService.drop_seq!('discussions_sequence_id', id)
-
end
-
-
1
def public?
-
1554
!private
-
end
-
-
1
def discussion
-
9760
self
-
end
-
-
1
def body=(val)
-
10
self.description=(val)
-
end
-
-
1
def body
-
562
self.description
-
end
-
-
1
def body_format
-
553
self.description_format
-
end
-
-
1
def body_format=(val)
-
5
self.description_format=(val)
-
end
-
-
1
def ranges
-
1042
RangeSet.parse(self.ranges_string)
-
end
-
-
1
def first_sequence_id
-
5
Array(ranges.first).first.to_i
-
end
-
-
1
def last_sequence_id
-
6
Array(ranges.last).last.to_i
-
end
-
-
# this is insted of a big slow migration
-
1
def ranges_string
-
2309
update_sequence_info! if self[:ranges_string].nil?
-
2309
self[:ranges_string]
-
end
-
-
1
def is_new_version?
-
(['title', 'description', 'private'] & self.changes.keys).any?
-
end
-
-
1
private
-
-
1
def set_last_activity_at_to_created_at
-
1018
update_column(:last_activity_at, created_at)
-
end
-
-
1
def sequence_id_or_0(item)
-
item.try(:sequence_id) || 0
-
end
-
-
1
def privacy_is_permitted_by_group
-
1553
if self.public? and group.private_discussions_only?
-
1
errors.add(:private, "must be private")
-
end
-
-
1553
if self.private? and group.public_discussions_only?
-
errors.add(:private, "must be public")
-
end
-
end
-
end
-
1
class DiscussionReader < ApplicationRecord
-
1
include CustomCounterCache::Model
-
1
include HasVolume
-
-
1
extend HasTokens
-
1
initialized_with_token :token
-
-
1
belongs_to :user
-
1
belongs_to :discussion
-
1
belongs_to :inviter, class_name: 'User'
-
-
1
delegate :message_channel, to: :user
-
-
1
scope :dangling, -> { joins('left join discussions on discussions.id = discussion_id left join users on users.id = user_id').where('discussions.id is null or users.id is null') }
-
-
1381
scope :active, -> { where("discussion_readers.revoked_at IS NULL") }
-
-
667
scope :guests, -> { active.where('discussion_readers.guest': true) }
-
1
scope :admins, -> { active.where('discussion_readers.admin': true) }
-
-
626
scope :redeemable, -> { guests.where('discussion_readers.accepted_at IS NULL') }
-
-
3
scope :redeemable_by, -> (user_id) { redeemable.joins(:user).where("user_id = ? OR users.email_verified = false", user_id) }
-
-
1
update_counter_cache :discussion, :seen_by_count
-
1
update_counter_cache :discussion, :members_count
-
-
1
def self.for(user:, discussion:)
-
1342
if user&.is_logged_in?
-
1342
find_or_initialize_by(user_id: user.id, discussion_id: discussion.id) do |dr|
-
762
m = user.memberships.find_by(group_id: discussion.group_id)
-
762
dr.volume = (m && m.volume) || 'normal'
-
end
-
else
-
new(discussion: discussion)
-
end
-
end
-
-
1
def self.for_model(model, actor = nil)
-
790
self.for(user: actor || model.author, discussion: model.discussion)
-
end
-
-
1
def update_reader(ranges: nil, volume: nil, participate: false, dismiss: false)
-
786
viewed!(ranges, persist: false) if ranges
-
786
set_volume!(volume, persist: false) if volume && (volume != :loud || user.email_on_participation?)
-
786
dismiss!(persist: false) if dismiss
-
786
save! if changed?
-
786
self
-
end
-
-
1
def viewed!(ranges = [], persist: true)
-
427
mark_as_read(ranges) unless has_read?(ranges)
-
427
assign_attributes(last_read_at: Time.now)
-
427
save if persist
-
end
-
-
1
def has_read?(ranges = [])
-
437
RangeSet.includes?(read_ranges, ranges)
-
end
-
-
1
def mark_as_read(ranges)
-
427
ranges = RangeSet.to_ranges(ranges)
-
427
return if ranges.empty?
-
427
self.read_ranges = read_ranges.concat(ranges)
-
end
-
-
1
def dismiss!(persist: true)
-
3
self.dismissed_at = Time.zone.now
-
3
save if persist
-
end
-
-
1
def recall!(persist: true)
-
1
self.dismissed_at = nil
-
1
save if persist
-
end
-
-
1
def computed_volume
-
8
if persisted?
-
7
volume || membership&.volume || 'normal'
-
else
-
1
membership.volume
-
end
-
end
-
-
1
def discussion_reader_volume
-
154
self[:volume]
-
end
-
-
1
def discussion_reader_user_id
-
154
self.user_id
-
end
-
-
1
def read_ranges
-
1075
RangeSet.parse(self.read_ranges_string)
-
end
-
-
1
def read_ranges=(ranges)
-
427
ranges = RangeSet.reduce(ranges)
-
427
self.read_ranges_string = RangeSet.serialize(ranges)
-
end
-
-
1
def first_unread_sequence_id
-
Array(unread_ranges.first).first.to_i
-
end
-
-
# maybe yagni, because the client should do this locally
-
1
def unread_ranges
-
RangeSet.subtract_ranges(discussion.ranges, read_ranges)
-
end
-
-
1
def read_ranges_string
-
1081
self[:read_ranges_string] ||= begin
-
511
if last_read_sequence_id == 0
-
511
""
-
else
-
"#{[discussion.first_sequence_id, 1].max}-#{last_read_sequence_id}"
-
end
-
end
-
end
-
-
1
def read_items_count
-
8
RangeSet.length(read_ranges)
-
end
-
-
1
def unread_items_count
-
RangeSet.length(unread_ranges)
-
end
-
-
1
private
-
1
def membership
-
1
@membership ||= discussion.group.membership_for(user)
-
end
-
end
-
1
class DiscussionTemplate < ApplicationRecord
-
1
include Discard::Model
-
1
include HasRichText
-
1
include CustomCounterCache::Model
-
-
1
is_rich_text on: :description
-
-
1
belongs_to :author, class_name: "User"
-
1
belongs_to :group, class_name: "Group"
-
-
1
update_counter_cache :group, :discussion_templates_count
-
-
1
validates :description, length: { maximum: Rails.application.secrets.max_message_length }
-
-
1
validates :process_name, presence: true
-
# validates :process_subtitle, presence: true
-
-
1
has_paper_trail only: [
-
:public,
-
:title,
-
:process_name,
-
:process_subtitle,
-
:process_introduction,
-
:process_introduction_format,
-
:description,
-
:description_format,
-
:group_id,
-
:tags,
-
:discarded_at
-
]
-
-
1
def members
-
User.none
-
end
-
-
1
def dump_i18n
-
out = {}
-
[
-
:title,
-
:title_placeholder,
-
:process_name,
-
:process_subtitle,
-
:process_introduction,
-
:description,
-
].map(&:to_s).each do |key|
-
value = self[key]
-
next unless value
-
value.strip! if value.respond_to? :strip!
-
out[key] = value
-
end
-
-
tags.each do |tag|
-
out[tag.underscore.gsub(" ", "_")] = tag
-
end
-
-
{process_name.strip.underscore.gsub(" ", "_") => out}
-
end
-
-
1
def poll_templates
-
PollTemplate.where(id: poll_template_ids)
-
end
-
-
1
def poll_template_ids
-
self.poll_template_keys_or_ids.filter do |key_or_id|
-
key_or_id.is_a? Integer
-
end
-
end
-
end
-
1
class Document < ApplicationRecord
-
1
belongs_to :model, polymorphic: true, required: false
-
1
belongs_to :author, class_name: 'User', required: true
-
1
validates :title, presence: true
-
1
validates :doctype, presence: true
-
1
validates :color, presence: true
-
1
before_validation :set_metadata
-
-
1
before_save :set_group_id
-
-
1
has_one_attached :file
-
-
1
scope :search_for, ->(query) {
-
5
if query.present?
-
where("title ilike :q", q: "%#{query}%")
-
else
-
5
all
-
end
-
}
-
-
1
def download_url
-
18
return nil unless file.attached?
-
Rails.application.routes.url_helpers.rails_blob_path(file, only_path: true)
-
end
-
-
1
def reset_metadata!
-
update(doctype: metadata['name'], icon: metadata['icon'], color: metadata['color'])
-
end
-
-
1
[:group, :discussion, :poll].map do |model_type|
-
21
define_method model_type, -> { self.model.send(model_type) if self.model.respond_to?(model_type) }
-
end
-
-
1
def is_an_image?
-
metadata['icon'] == 'image'
-
end
-
-
1
def url
-
513
return file.url if file.attached?
-
513
return nil unless self[:url]
-
513
self[:url].to_s.starts_with?("http") ? self[:url] : "#{lmo_asset_host}#{self[:url]}"
-
end
-
-
1
private
-
-
1
def set_group_id
-
34
self.group_id = model.group_id if model && model.respond_to?(:group_id)
-
end
-
-
1
def set_metadata
-
34
self.doctype ||= metadata['name']
-
34
self.icon ||= metadata['icon']
-
34
self.color ||= metadata['color']
-
end
-
-
1
def metadata
-
612
@metadata ||= Hash(AppConfig.doctypes.detect { |type| /#{type['regex']}/.match(file.content_type || url) })
-
end
-
end
-
class Event < ApplicationRecord
-
include ActionView::Helpers::SanitizeHelper
-
include CustomCounterCache::Model
-
include HasTimeframe
-
extend HasCustomFields
-
-
has_many :notifications, dependent: :destroy
-
belongs_to :eventable, polymorphic: true
-
belongs_to :discussion, required: false
-
belongs_to :user, required: false
-
belongs_to :parent, class_name: "Event", required: false
-
has_many :children, (-> { where("discussion_id is not null") }), class_name: "Event", foreign_key: :parent_id
-
set_custom_fields :pinned_title, :recipient_user_ids, :recipient_chatbot_ids, :recipient_message, :recipient_audience, :stance_ids
-
-
before_create :set_parent_and_depth, if: :discussion_id
-
before_create :set_sequences, if: :discussion_id
-
after_rollback :reset_sequences, if: :discussion_id
-
before_destroy :reset_sequences, if: :discussion_id
-
-
after_create :update_sequence_info!, if: :discussion_id
-
after_destroy :update_sequence_info!, if: :discussion_id
-
-
define_counter_cache(:child_count) { |e| e.children.count }
-
define_counter_cache(:descendant_count) { |e|
-
if e.kind == "new_discussion"
-
Event.where(discussion_id: e.eventable_id).count
-
elsif e.position_key && e.discussion_id
-
Event.where(discussion_id: e.discussion_id).
-
where("id != ?", e.id).
-
where('position_key like ?', e.position_key+"%").count
-
else
-
0
-
end
-
}
-
update_counter_cache :parent, :child_count
-
update_counter_cache :parent, :descendant_count
-
-
validates :kind, presence: true
-
validates :eventable, presence: true
-
-
scope :dangling, -> { joins('left join discussions d on events.discussion_id = d.id').where('d.id is null and discussion_id is not null') }
-
scope :unreadable, -> { where.not(kind: 'discussion_closed') }
-
-
scope :invitations_in_period, ->(since, till) {
-
where(kind: :announcement_created, eventable_type: 'Group').within(since.beginning_of_hour, till.beginning_of_hour)
-
}
-
-
delegate :group, to: :eventable, allow_nil: true
-
delegate :poll, to: :eventable, allow_nil: true
-
delegate :groups, to: :eventable, allow_nil: true
-
delegate :update_sequence_info!, to: :discussion, allow_nil: true
-
-
def self.sti_find(id)
-
e = self.find(id)
-
e.kind_class.find(id)
-
end
-
-
def kind_class
-
("Events::"+kind.classify).constantize
-
end
-
-
def self.publish!(eventable, **args)
-
event = build(eventable, **args)
-
event.save!
-
PublishEventWorker.perform_async(event.id)
-
event
-
end
-
-
def self.build(eventable, **args)
-
new({
-
kind: name.demodulize.underscore,
-
eventable: eventable,
-
eventable_version_id: ((eventable.respond_to?(:versions) && eventable.versions.last&.id) || nil)
-
}.merge(args))
-
end
-
-
def user
-
super || AnonymousUser.new
-
end
-
-
def real_user
-
user
-
end
-
-
def actor
-
user
-
end
-
-
def actor_id
-
user_id
-
end
-
-
def message_channel
-
eventable.group.message_channel
-
end
-
-
# this is called after create, and calls methods defined by the event concerns
-
# included per event type
-
def trigger!
-
EventBus.broadcast("#{kind}_event", self)
-
end
-
-
def active_model_serializer
-
"Events::#{eventable.class.to_s.split('::').last}Serializer".constantize
-
rescue NameError
-
EventSerializer
-
end
-
-
def set_parent_and_depth
-
self.parent = max_depth_adjusted_parent
-
self.depth = parent ? parent.depth + 1 : 0
-
end
-
-
def set_parent_and_depth!
-
set_parent_and_depth
-
update_columns(parent_id: parent_id, depth: depth)
-
end
-
-
def set_sequences
-
self.sequence_id = next_sequence_id!
-
self.position = next_position!
-
# self.position_key = self_and_parents.reverse.map(&:position).map{|p| Event.zero_fill(p) }.join('-')
-
self.position_key = [parent&.position_key, Event.zero_fill(position)].compact.join('-')
-
end
-
-
def set_sequence_id!
-
update_attribute(:sequence_id, next_sequence_id!)
-
end
-
-
def reset_sequences
-
SequenceService.drop_seq!('discussions_sequence_id', discussion_id)
-
EventService.reset_child_positions(parent.id, parent.position_key) if parent_id && parent
-
end
-
-
def next_sequence_id!
-
unless SequenceService.seq_present?('discussions_sequence_id', discussion_id)
-
val = Event.
-
where(discussion_id: discussion_id).
-
where("sequence_id is not null").
-
order(sequence_id: :desc).
-
limit(1).pluck(:sequence_id).last || 0
-
SequenceService.create_seq!('discussions_sequence_id', discussion_id, val)
-
end
-
SequenceService.next_seq!('discussions_sequence_id', discussion_id)
-
end
-
-
def next_position!
-
return 0 unless (discussion_id and parent_id)
-
unless SequenceService.seq_present?('events_position', parent_id)
-
val = Event.where(parent_id: parent_id,
-
discussion_id: discussion_id).
-
order(position: :desc).
-
limit(1).pluck(:position).last || 0
-
SequenceService.create_seq!('events_position', parent_id, val)
-
end
-
SequenceService.next_seq!('events_position', parent_id)
-
end
-
-
def self.zero_fill(num)
-
"0" * (5 - num.to_s.length) + num.to_s
-
end
-
-
def find_parent_event
-
case kind
-
when 'discussion_closed' then eventable.created_event
-
when 'discussion_forked' then eventable.created_event
-
when 'discussion_moved' then discussion.created_event
-
when 'discussion_edited' then (eventable || discussion)&.created_event
-
when 'discussion_reopened' then eventable.created_event
-
when 'outcome_created' then eventable.parent_event
-
when 'new_comment' then eventable.parent_event
-
when 'poll_closed_by_user' then eventable.created_event
-
when 'poll_closing_soon' then eventable.created_event
-
when 'poll_created' then eventable.parent_event
-
when 'poll_edited' then eventable.created_event
-
when 'poll_expired' then eventable.created_event
-
when 'poll_option_added' then eventable.created_event
-
when 'poll_reopened' then eventable.created_event
-
when 'stance_created' then eventable.parent_event
-
when 'stance_updated' then eventable.parent_event
-
else
-
nil
-
end
-
end
-
-
def self_and_parents
-
[self, (parent && parent.discussion_id && parent.self_and_parents)].flatten.compact
-
end
-
-
def max_depth_adjusted_parent
-
original_parent = find_parent_event
-
return nil unless original_parent
-
if discussion && discussion.max_depth == original_parent.depth
-
original_parent.parent
-
else
-
original_parent
-
end
-
end
-
-
def email_recipients
-
Queries::UsersByVolumeQuery.email_notifications(eventable).where(id: all_recipient_user_ids)
-
end
-
-
def notification_recipients
-
Queries::UsersByVolumeQuery.app_notifications(eventable).where(id: all_recipient_user_ids).where.not(id: user.id || 0)
-
end
-
-
def all_recipients
-
User.active.where(id: all_recipient_user_ids)
-
end
-
-
def all_recipient_user_ids
-
(recipient_user_ids || []).uniq.compact #.without(actor_id)
-
end
-
end
-
class Events::AnnouncementResend < Event
-
include Events::Notify::ByEmail
-
-
def self.publish!(event)
-
super event.eventable,
-
user: event.user,
-
custom_fields: {
-
membership_ids: Membership.pending.where(id: event.custom_fields['membership_ids']).pluck(:id),
-
kind: event.custom_fields['kind']
-
}
-
end
-
-
def email_method
-
'group_announced'
-
end
-
-
def email_recipients
-
# return User.none if eventable.is_a?(Poll) && !eventable.active? # do we want this?
-
User.active.where(id: Membership.where(id: custom_fields['membership_ids']).pluck(:user_id))
-
end
-
end
-
1
class Events::CommentEdited < Event
-
1
include Events::LiveUpdate
-
1
include Events::Notify::Mentions
-
-
1
def self.publish!(comment, actor)
-
5
super(comment, user: actor)
-
end
-
end
-
1
class Events::CommentRepliedTo < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
-
1
def self.publish!(comment)
-
5
super comment, user: comment.author
-
end
-
-
1
private
-
-
1
def email_recipients
-
5
notification_recipients.where(email_when_mentioned: true)
-
end
-
-
1
def notification_recipients
-
10
eventable.members.where('users.id': eventable.parent.author_id).where.not('users.id': eventable.author_id)
-
end
-
end
-
1
class Events::DiscussionAnnounced < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::Chatbots
-
-
1
def self.publish!(
-
discussion:,
-
actor:,
-
recipient_user_ids:,
-
recipient_chatbot_ids:,
-
recipient_audience: nil,
-
recipient_message: nil)
-
-
9
super(discussion,
-
user: actor,
-
recipient_user_ids: recipient_user_ids,
-
recipient_chatbot_ids: recipient_chatbot_ids,
-
recipient_audience: recipient_audience.presence,
-
recipient_message: recipient_message.presence)
-
end
-
end
-
class Events::DiscussionClosed < Event
-
include Events::LiveUpdate
-
-
def self.publish!(discussion, actor)
-
super discussion,
-
user: actor,
-
discussion: discussion,
-
created_at: discussion.closed_at
-
end
-
end
-
class Events::DiscussionDescriptionEdited < Event
-
def self.publish!(discussion, editor)
-
super discussion, user: editor
-
end
-
end
-
1
class Events::DiscussionEdited < Event
-
1
include Events::LiveUpdate
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::Mentions
-
1
include Events::Notify::Chatbots
-
-
1
def self.publish!(
-
discussion:,
-
actor:,
-
recipient_user_ids: [],
-
recipient_chatbot_ids: [],
-
recipient_audience: nil,
-
recipient_message: nil)
-
9
super(discussion,
-
user: actor,
-
9
discussion_id: (recipient_message && discussion.id) || nil,
-
recipient_user_ids: recipient_user_ids,
-
recipient_chatbot_ids: recipient_chatbot_ids,
-
recipient_audience: recipient_audience,
-
recipient_message: recipient_message)
-
end
-
-
1
def discussion
-
18
eventable
-
end
-
end
-
class Events::DiscussionForked < Event
-
def self.publish!(discussion, source)
-
super discussion,
-
discussion: source,
-
user: discussion.author,
-
sequence_id: discussion.forked_items.minimum(:sequence_id)+1,
-
created_at: discussion.created_at,
-
custom_fields: { item_ids: discussion.forked_event_ids }
-
end
-
end
-
1
class Events::DiscussionMoved < Event
-
1
include Events::LiveUpdate
-
-
1
def self.publish!(discussion, actor, source_group)
-
7
super discussion,
-
discussion: discussion,
-
custom_fields: { source_group_id: source_group.id },
-
user: actor,
-
created_at: Time.now
-
end
-
end
-
class Events::DiscussionReopened < Event
-
include Events::LiveUpdate
-
-
def self.publish!(discussion, actor)
-
super discussion,
-
user: actor,
-
discussion: discussion
-
end
-
end
-
class Events::DiscussionTitleEdited < Event
-
def self.publish!(discussion, editor)
-
super discussion, user: editor, created_at: Time.now
-
end
-
end
-
class Events::GroupIdentityCreated < Event
-
def self.publish!(group_identity, actor)
-
super group_identity,
-
user: actor,
-
announcement: group_identity.make_announcement
-
end
-
-
def identities
-
return Identities::Base.none unless announcement
-
super.where(id: eventable.identity_id)
-
end
-
end
-
1
class Events::InvitationAccepted < Event
-
1
include Events::Notify::InApp
-
1
include Events::LiveUpdate
-
-
1
def self.publish!(membership)
-
11
super membership, user: membership.user
-
end
-
-
1
private
-
-
1
def notification_recipients
-
11
User.where(id: eventable.inviter_id)
-
end
-
-
1
def notification_actor
-
10
eventable&.user
-
end
-
-
1
def notification_url
-
5
polymorphic_url(eventable.group)
-
end
-
end
-
1
class Events::MembershipCreated < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
-
1
def self.publish!(
-
group:,
-
actor:,
-
recipient_user_ids:,
-
recipient_message: nil)
-
13
super group,
-
user: actor,
-
recipient_message: recipient_message,
-
recipient_user_ids: recipient_user_ids
-
end
-
end
-
1
class Events::MembershipRequestApproved < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
-
1
def self.publish!(membership, approver)
-
1
super membership, user: approver
-
end
-
-
1
private
-
1
def email_users!
-
1
email_recipients.active.uniq.pluck(:id).each do |recipient_id|
-
1
EventMailer.event(recipient_id, self.id).deliver_later
-
end
-
end
-
-
1
def notification_recipients
-
2
User.where(id: eventable&.user_id)
-
end
-
1
alias :email_recipients :notification_recipients
-
end
-
class Events::MembershipRequested < Event
-
include Events::Notify::InApp
-
include Events::Notify::ByEmail
-
-
def self.publish!(membership_request)
-
super membership_request, user: membership_request.requestor
-
end
-
-
private
-
-
def notification_recipients
-
eventable.admins.active
-
end
-
-
def email_recipients
-
Queries::UsersByVolumeQuery.
-
email_notifications(eventable.group).
-
where(id: eventable.admins.active.pluck(:id))
-
end
-
-
def notification_actor
-
eventable.requestor
-
end
-
-
def notification_translation_values
-
{ name: eventable.requestor&.name || eventable.name, title: eventable.group.full_name }
-
end
-
end
-
1
class Events::MembershipResent < Event
-
1
include Events::Notify::ByEmail
-
-
1
def self.publish!(membership, actor)
-
1
super membership.group,
-
user: actor,
-
custom_fields: { membership_id: membership.id }
-
end
-
-
1
private
-
-
1
def email_method
-
:"#{eventable_key}_announced"
-
end
-
-
1
def email_recipients
-
1
User.where(id: membership.user_id)
-
end
-
-
1
def eventable_key
-
return :group if eventable.is_a?(Group)
-
eventable.class.to_s.downcase
-
end
-
-
1
def membership
-
1
Membership.find(custom_fields['membership_id'])
-
end
-
end
-
1
class Events::NewComment < Event
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::Mentions
-
1
include Events::Notify::Chatbots
-
1
include Events::LiveUpdate
-
-
1
def self.publish!(comment)
-
183
if comment.parent.present?
-
183
GenericWorker.perform_async('NotificationService', 'mark_as_read', comment.parent_type, comment.parent_id, comment.author_id)
-
end
-
-
183
super comment,
-
user: comment.author,
-
discussion: comment.discussion,
-
pinned: comment.should_pin
-
end
-
-
1
private
-
1
def email_recipients
-
184
Queries::UsersByVolumeQuery.loud(eventable.discussion)
-
.where.not(id: eventable.author)
-
.where.not(id: eventable.mentioned_users)
-
.where.not(id: eventable.parent_author).distinct
-
end
-
end
-
1
class Events::NewCoordinator < Event
-
1
include Events::Notify::InApp
-
-
1
def self.publish!(membership, actor)
-
2
super membership, user: actor, created_at: Time.now
-
end
-
-
1
private
-
-
1
def notification_recipients
-
2
User.where(id: eventable.user_id)
-
end
-
end
-
1
class Events::NewDiscussion < Event
-
1
include Events::LiveUpdate
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::Mentions
-
1
include Events::Notify::Chatbots
-
-
1
def self.publish!(
-
discussion:,
-
recipient_user_ids: [],
-
recipient_chatbot_ids: [],
-
recipient_audience: nil)
-
-
364
super(discussion,
-
user: discussion.author,
-
recipient_user_ids: recipient_user_ids,
-
recipient_chatbot_ids: recipient_chatbot_ids,
-
recipient_audience: recipient_audience.presence)
-
end
-
-
1
def discussion
-
728
eventable
-
end
-
end
-
1
class Events::OutcomeAnnounced < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
-
1
def self.publish!(outcome, actor, user_ids, audience = nil)
-
15
super outcome,
-
user: actor,
-
recipient_user_ids: user_ids.uniq.compact,
-
recipient_audience: audience.presence
-
end
-
end
-
1
class Events::OutcomeCreated < Event
-
1
include Events::Notify::Mentions
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::Chatbots
-
1
include Events::LiveUpdate
-
-
1
def self.publish!(
-
outcome:,
-
recipient_user_ids: [],
-
recipient_chatbot_ids: [],
-
recipient_audience: nil)
-
24
super(outcome,
-
user: outcome.author,
-
discussion: outcome.poll.discussion,
-
recipient_user_ids: recipient_user_ids,
-
recipient_chatbot_ids: recipient_chatbot_ids,
-
recipient_audience: recipient_audience)
-
end
-
end
-
1
class Events::OutcomeReviewDue < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::Chatbots
-
-
1
def self.publish!(outcome)
-
8
super outcome,
-
user: outcome.author
-
end
-
-
1
private
-
1
def email_recipients
-
8
Queries::UsersByVolumeQuery.email_notifications(poll)
-
.where('users.id': raw_recipients.pluck(:id))
-
end
-
-
1
def notification_recipients
-
8
Queries::UsersByVolumeQuery.app_notifications(poll)
-
.where('users.id': raw_recipients.pluck(:id))
-
end
-
-
1
def raw_recipients
-
16
User.where(id: eventable.author_id)
-
end
-
end
-
1
class Events::OutcomeUpdated < Event
-
1
include Events::Notify::Mentions
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::Chatbots
-
1
include Events::LiveUpdate
-
-
1
def self.publish!(outcome:,
-
actor:,
-
recipient_user_ids: [],
-
recipient_chatbot_ids: [],
-
recipient_audience: nil)
-
1
super(outcome,
-
user: actor,
-
recipient_user_ids: recipient_user_ids,
-
recipient_chatbot_ids: recipient_chatbot_ids,
-
recipient_audience: recipient_audience)
-
end
-
end
-
1
class Events::PollAnnounced < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::Chatbots
-
-
1
def self.publish!(
-
poll: ,
-
actor: ,
-
stances: ,
-
recipient_user_ids: [],
-
recipient_chatbot_ids: [],
-
recipient_audience: nil,
-
recipient_message: nil)
-
-
4
super poll,
-
user: actor,
-
stance_ids: stances.map(&:id),
-
recipient_user_ids: recipient_user_ids,
-
recipient_chatbot_ids: recipient_chatbot_ids,
-
recipient_audience: recipient_audience.presence,
-
recipient_message: recipient_message.presence
-
end
-
-
1
private
-
-
1
def stances
-
Stance.where(id: self.stance_ids)
-
end
-
-
1
def email_recipients
-
4
notification_recipients.where(id: Queries::UsersByVolumeQuery.normal_or_loud(eventable))
-
end
-
-
1
def notification_recipients
-
8
User.active.distinct.joins(:stances).where('stances.id IN (?)', self.stance_ids)
-
end
-
end
-
1
class Events::PollClosedByUser < Event
-
1
include Events::LiveUpdate
-
1
include Events::Notify::Chatbots
-
-
1
def self.publish!(poll, actor)
-
6
super poll,
-
user: actor,
-
discussion: poll.discussion,
-
created_at: poll.closed_at
-
end
-
end
-
1
class Events::PollClosingSoon < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::Author
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::Chatbots
-
-
1
def self.publish!(poll)
-
32
super poll,
-
user: poll.author,
-
created_at: Time.now
-
end
-
-
1
def notify_author?
-
33
eventable.notify_on_closing_soon == 'author'
-
end
-
-
1
private
-
1
def email_recipients
-
35
Queries::UsersByVolumeQuery.email_notifications(poll)
-
.where('users.id': raw_recipients.pluck(:id))
-
end
-
-
1
def notification_recipients
-
35
Queries::UsersByVolumeQuery.app_notifications(poll)
-
.where('users.id': raw_recipients.pluck(:id))
-
end
-
-
1
def raw_recipients
-
# work around for anonymous
-
70
case poll.notify_on_closing_soon
-
# when 'author'
-
# User.where(id: poll.author_id)
-
when 'undecided_voters'
-
4
poll.unmasked_undecided_voters
-
when 'voters'
-
46
poll.unmasked_voters
-
else
-
20
User.none
-
end
-
end
-
-
1
def notification_translation_values
-
309
super.merge(poll_type: I18n.t(:"poll_types.#{eventable.poll_type}"))
-
end
-
end
-
1
class Events::PollCreated < Event
-
1
include Events::LiveUpdate
-
1
include Events::Notify::Mentions
-
1
include Events::Notify::Chatbots
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::InApp
-
-
1
def self.publish!(poll, actor, recipient_user_ids: [])
-
168
super poll,
-
user: actor,
-
discussion: poll.discussion,
-
pinned: true,
-
recipient_user_ids: recipient_user_ids
-
end
-
end
-
1
class Events::PollEdited < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::Mentions
-
1
include Events::Notify::Chatbots
-
-
1
def self.publish!(
-
poll:,
-
actor:,
-
recipient_user_ids: [],
-
recipient_chatbot_ids: [],
-
recipient_message: nil,
-
recipient_audience: nil)
-
7
super(poll,
-
7
discussion_id: (recipient_message && poll.discussion_id) || nil,
-
user: actor,
-
recipient_user_ids: recipient_user_ids,
-
recipient_chatbot_ids: recipient_chatbot_ids,
-
recipient_audience: recipient_audience.presence,
-
recipient_message: recipient_message.presence)
-
end
-
end
-
1
class Events::PollExpired < Event
-
1
include Events::Notify::Author
-
1
include Events::Notify::Chatbots
-
1
include Events::Notify::InApp
-
-
1
def self.publish!(poll)
-
18
super poll,
-
user: poll.author,
-
discussion: nil,
-
created_at: poll.closed_at
-
end
-
-
# email the author and create an in-app notification
-
1
def email_author!
-
18
super
-
18
notification_for(author).save
-
end
-
-
1
def notify_author?
-
18
return false unless eventable.present? && eventable.poll.present?
-
18
Queries::UsersByVolumeQuery.email_notifications(eventable).exists?(eventable.poll.author_id)
-
end
-
end
-
class Events::PollOptionAdded < Event
-
include Events::Notify::Author
-
include Events::Notify::InApp
-
-
def self.publish!(poll, actor, poll_option_names = [])
-
return unless Array(poll_option_names).any?
-
super poll,
-
user: (actor unless poll.anonymous?),
-
custom_fields: { poll_option_names: poll_option_names }
-
end
-
-
private
-
-
def notify_author?
-
Queries::UsersByVolumeQuery.email_notifications(eventable).exists?(poll.author_id)
-
end
-
-
def notification_recipients
-
User.where(id: eventable.author_id)
-
end
-
end
-
class Events::PollReminder < Event
-
include Events::Notify::InApp
-
include Events::Notify::ByEmail
-
include Events::Notify::Chatbots
-
-
def self.publish!(
-
poll:,
-
actor:,
-
recipient_user_ids: [],
-
recipient_chatbot_ids: [],
-
recipient_message: nil,
-
recipient_audience: nil)
-
super(poll,
-
discussion_id: nil,
-
user: actor,
-
recipient_user_ids: recipient_user_ids,
-
recipient_chatbot_ids: recipient_chatbot_ids,
-
recipient_audience: recipient_audience.presence,
-
recipient_message: recipient_message.presence)
-
end
-
end
-
1
class Events::PollReopened < Event
-
1
include Events::Notify::Chatbots
-
-
1
def self.publish!(poll, actor)
-
1
create(kind: "poll_reopened",
-
user: actor,
-
discussion: poll.discussion,
-
1
eventable: poll).tap { |e| EventBus.broadcast('poll_reopened_event', e) }
-
end
-
end
-
1
class Events::ReactionCreated < Event
-
1
include Events::Notify::InApp
-
1
include Events::LiveUpdate
-
1
include PrettyUrlHelper
-
-
1
def self.publish!(reaction)
-
4
super reaction, user: reaction.user
-
end
-
-
1
private
-
-
1
def notification_recipients
-
4
return User.none if !reactable || # there is no reactable
-
reactable.author == user || # you liked your own reactable
-
!reactable.group.memberships.find_by(user: reactable.author) # the author has left the group
-
2
User.where(id: reactable.author_id)
-
end
-
-
1
def notification_translation_values
-
2
super.merge(
-
reaction: eventable.reaction.downcase,
-
model: I18n.t(:"notification_models.#{reactable.class.to_s.downcase}")
-
)
-
end
-
-
1
def reactable
-
18
@reactable ||= eventable&.reactable
-
end
-
end
-
1
class Events::StanceCreated < Event
-
1
include Events::LiveUpdate
-
1
include Events::Notify::ByEmail
-
1
include Events::Notify::InApp
-
1
include Events::Notify::Mentions
-
1
include Events::Notify::Chatbots
-
-
1
def self.publish!(stance)
-
68
GenericWorker.perform_async('NotificationService', 'mark_as_read', "Poll", stance.poll_id, stance.participant_id)
-
-
68
super stance,
-
user: stance.participant.presence,
-
68
discussion: stance.add_to_discussion? ? stance.poll.discussion : nil
-
end
-
-
1
def notify_mentions!
-
68
return if eventable.poll.anonymous || eventable.poll.hide_results == 'until_closed'
-
37
super
-
end
-
-
1
def real_user
-
eventable.real_participant
-
end
-
-
1
private
-
-
1
def notification_translation_values
-
{
-
name: eventable.participant.name,
-
title: eventable.poll.title
-
}
-
end
-
-
1
def notification_url
-
@notification_url ||= polymorphic_url(eventable.poll)
-
end
-
-
1
def email_recipients
-
71
Queries::UsersByVolumeQuery.loud(eventable.poll)
-
.where.not(id: eventable.author)
-
.where.not(id: eventable.mentioned_users).distinct
-
end
-
end
-
class Events::StanceUpdated < Events::StanceCreated
-
end
-
1
class Events::UnknownSender < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
-
1
def self.publish!(received_email)
-
2
super received_email
-
end
-
-
1
def wait_time
-
0.minute
-
end
-
-
1
private
-
-
1
def notification_recipients
-
4
eventable.group.admins.active
-
end
-
-
1
def email_recipients
-
2
Queries::UsersByVolumeQuery.
-
email_notifications(eventable.group).
-
where(id: notification_recipients.pluck(:id))
-
end
-
-
1
def notification_actor
-
nil
-
end
-
-
1
def notification_translation_values
-
2
{ name: notification_actor&.name, title: eventable.group.full_name }
-
end
-
end
-
1
class Events::UserAddedToGroup < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
-
1
def self.publish!(membership, inviter)
-
super membership, user: inviter
-
end
-
-
1
private
-
-
1
def notification_recipients
-
User.where(id: eventable.user_id)
-
end
-
1
alias :email_recipients :notification_recipients
-
-
1
def notification_actor
-
eventable.inviter
-
end
-
end
-
class Events::UserJoinedGroup < Event
-
def self.publish!(membership)
-
super membership, user: membership.user
-
end
-
end
-
1
class Events::UserMentioned < Event
-
1
include Events::Notify::InApp
-
1
include Events::Notify::ByEmail
-
-
1
def self.publish!(model, actor, users)
-
27
super model,
-
user: actor,
-
custom_fields: { user_ids: users.pluck(:id) }
-
end
-
-
1
private
-
1
def email_recipients
-
28
notification_recipients.where(email_when_mentioned: true)
-
end
-
-
1
def notification_recipients
-
56
User.where(id: custom_fields['user_ids'])
-
end
-
end
-
class Events::UserReactivated < Event
-
include Events::Notify::ByEmail
-
-
def self.publish!(user)
-
super user, user: user
-
end
-
-
def email_users!
-
eventable.send(:mailer).delay(queue: :critical).send(email_method, user, self)
-
end
-
-
end
-
class FormalGroup < Group
-
end
-
class ForwardEmailRule < ApplicationRecord
-
def self.ransackable_attributes(auth_object = nil)
-
["email", "handle", "id"]
-
end
-
end
-
1
class GlobalMessageChannel
-
1
include Singleton
-
-
1
def message_channel
-
'/global'
-
end
-
end
-
class Group < ApplicationRecord
-
include HasTimeframe
-
include HasRichText
-
include CustomCounterCache::Model
-
include ReadableUnguessableUrls
-
include SelfReferencing
-
include MessageChannel
-
include GroupPrivacy
-
include HasEvents
-
include Translatable
-
-
extend HasTokens
-
extend NoSpam
-
-
is_rich_text on: :description
-
is_translatable on: :description
-
initialized_with_token :token
-
no_spam_for :name, :description
-
-
belongs_to :creator, class_name: 'User'
-
alias_method :author, :creator
-
-
belongs_to :parent, class_name: 'Group'
-
scope :dangling, -> { joins('left join groups parents on parents.id = groups.parent_id').where('groups.parent_id is not null and parents.id is null') }
-
scope :empty_no_subscription, -> { joins('left join subscriptions on subscription_id = groups.subscription_id').where('subscriptions.id is null and groups.parent_id is null').where('memberships_count < 2 AND discussions_count < 3 and polls_count < 2 and subgroups_count = 0').where('groups.created_at < ?', 1.year.ago) }
-
scope :expired_trial, -> { joins(:subscription).where('subscriptions.plan = ?', 'trial').where('subscriptions.expires_at < ?', 12.months.ago) }
-
scope :any_trial, -> { joins(:subscription).where('subscriptions.plan = ?', 'trial') }
-
scope :expired_demo, -> { joins(:subscription).where('subscriptions.plan = ?', 'demo').where('groups.created_at < ?', 7.days.ago) }
-
scope :not_demo, -> { joins(:subscription).where('subscriptions.plan != ?', 'demo') }
-
-
has_many :discussions, dependent: :destroy
-
has_many :discussion_templates, dependent: :destroy
-
has_many :public_discussions, -> { visible_to_public }, foreign_key: :group_id, class_name: 'Discussion'
-
has_many :comments, through: :discussions
-
-
has_many :all_memberships, dependent: :destroy, class_name: 'Membership'
-
has_many :all_members, through: :all_memberships, source: :user
-
-
has_many :memberships, -> { active }
-
has_many :members, through: :memberships, source: :user
-
-
has_many :accepted_memberships, -> { active.accepted }, class_name: "Membership"
-
has_many :accepted_members, through: :accepted_memberships, source: :user
-
-
has_many :admin_memberships, -> { active.where(admin: true) }, class_name: 'Membership'
-
has_many :admins, through: :admin_memberships, source: :user
-
-
has_many :membership_requests, dependent: :destroy
-
has_many :pending_membership_requests, -> { where response: nil }, class_name: 'MembershipRequest'
-
-
has_many :polls, dependent: :destroy
-
has_many :poll_templates, dependent: :destroy
-
-
has_many :documents, as: :model, dependent: :destroy
-
has_many :requested_users, through: :membership_requests, source: :user
-
has_many :comments, through: :discussions
-
has_many :public_comments, through: :public_discussions, source: :comments
-
-
has_many :group_identities, dependent: :destroy, foreign_key: :group_id
-
has_many :identities, through: :group_identities
-
has_many :chatbots, dependent: :destroy
-
-
has_many :discussion_documents, through: :discussions, source: :documents
-
has_many :poll_documents, through: :polls, source: :documents
-
has_many :comment_documents, through: :comments, source: :documents
-
has_many :tags, foreign_key: :group_id
-
-
belongs_to :subscription
-
-
has_many :subgroups,
-
-> { where(archived_at: nil) },
-
class_name: 'Group',
-
foreign_key: 'parent_id'
-
has_many :all_subgroups, dependent: :destroy, class_name: 'Group', foreign_key: :parent_id
-
include GroupExportRelations
-
-
scope :with_serializer_includes, -> { includes(:subscription) }
-
scope :archived, -> { where('archived_at IS NOT NULL') }
-
scope :published, -> { where(archived_at: nil) }
-
scope :parents_only, -> { where(parent_id: nil) }
-
scope :visible_to_public, -> { published.where(is_visible_to_public: true) }
-
scope :hidden_from_public, -> { published.where(is_visible_to_public: false) }
-
scope :in_organisation, ->(group) { where(id: group.id_and_subgroup_ids) }
-
-
scope :explore_search, ->(query) { where("name ilike :q or description ilike :q", q: "%#{query}%") }
-
-
scope :by_slack_team, ->(team_id) {
-
joins(:identities)
-
.where("(omniauth_identities.custom_fields->'slack_team_id')::jsonb ? :team_id", team_id: team_id)
-
}
-
-
scope :by_slack_channel, ->(channel_id) {
-
joins(:group_identities)
-
.where("(group_identities.custom_fields->'slack_channel_id')::jsonb ? :channel_id", channel_id: channel_id)
-
}
-
-
scope :search_for, ->(query) { where("name ilike :q", q: "%#{query}%") }
-
-
validates_presence_of :name
-
validates :name, length: { maximum: 250 }
-
-
validate :limit_inheritance
-
validates :subscription, absence: true, if: :is_subgroup?
-
validate :handle_is_valid
-
validates :handle, uniqueness: true, allow_nil: true
-
-
delegate :locale, to: :creator, allow_nil: true
-
delegate :time_zone, to: :creator, allow_nil: true
-
delegate :date_time_pref, to: :creator, allow_nil: true
-
-
define_counter_cache(:polls_count) { |g| g.polls.count }
-
define_counter_cache(:closed_polls_count) { |g| g.polls.closed.count }
-
define_counter_cache(:poll_templates_count) { |g| g.poll_templates.kept.count }
-
define_counter_cache(:memberships_count) { |g| g.memberships.count }
-
define_counter_cache(:pending_memberships_count) { |g| g.memberships.pending.count }
-
define_counter_cache(:admin_memberships_count) { |g| g.admin_memberships.count }
-
define_counter_cache(:public_discussions_count) { |g| g.discussions.visible_to_public.count }
-
define_counter_cache(:discussions_count) { |g| g.discussions.kept.count }
-
define_counter_cache(:open_discussions_count) { |g| g.discussions.is_open.count }
-
define_counter_cache(:closed_discussions_count) { |g| g.discussions.is_closed.count }
-
define_counter_cache(:discussion_templates_count) { |g| g.discussion_templates.kept.count }
-
define_counter_cache(:subgroups_count) { |g| g.subgroups.published.count }
-
update_counter_cache(:parent, :subgroups_count)
-
-
delegate :include?, to: :users, prefix: true
-
delegate :members, to: :parent, prefix: true
-
-
has_one_attached :cover_photo, dependent: :detach
-
has_one_attached :logo, dependent: :detach
-
-
has_paper_trail only: [:name,
-
:parent_id,
-
:description,
-
:description_format,
-
:handle,
-
:archived_at,
-
:parent_members_can_see_discussions,
-
:key,
-
:is_visible_to_public,
-
:is_visible_to_parent_members,
-
:discussion_privacy_options,
-
:members_can_add_members,
-
:membership_granted_upon,
-
:members_can_edit_discussions,
-
:members_can_edit_comments,
-
:members_can_delete_comments,
-
:members_can_raise_motions,
-
:members_can_start_discussions,
-
:members_can_create_subgroups,
-
:creator_id,
-
:subscription_id,
-
:members_can_announce,
-
:new_threads_max_depth,
-
:new_threads_newest_first,
-
:admins_can_edit_user_content,
-
:listed_in_explore]
-
-
validates :description, length: { maximum: Rails.application.secrets.max_message_length }
-
before_validation :ensure_handle_is_not_empty
-
-
def logo_url(size = 512)
-
return nil unless logo.attached?
-
size = size.to_i
-
Rails.application.routes.url_helpers.rails_representation_path(
-
logo.representation(resize_to_limit: [size,size], saver: {quality: 80, strip: true}),
-
only_path: true
-
)
-
rescue ActiveStorage::UnrepresentableError
-
self.cover_photo.delete
-
nil
-
end
-
-
def cover_url(size = 512) # 2048x512 or 1024x256 normal res
-
size = size.to_i
-
return nil unless cover_photo.attached?
-
Rails.application.routes.url_helpers.rails_representation_path(
-
cover_photo.representation(HasRichText::PREVIEW_OPTIONS.merge(resize_to_limit: [size*4,size])),
-
only_path: true
-
)
-
rescue ActiveStorage::UnrepresentableError
-
self.cover_photo.delete
-
nil
-
end
-
-
def self_or_parent_logo_url(size = 512)
-
logo_url(size) || (parent && parent.logo_url(size))
-
end
-
-
def self_or_parent_cover_url(size = 512)
-
cover_url(size) || (parent && parent.cover_url(size))
-
end
-
-
def existing_member_ids
-
member_ids
-
end
-
-
def author_id
-
creator_id
-
end
-
-
def user_id
-
creator_id
-
end
-
-
def discussion_id
-
nil
-
end
-
-
def accepted_memberships_count
-
memberships_count - pending_memberships_count
-
end
-
-
def poll_id
-
nil
-
end
-
-
def poll
-
nil
-
end
-
-
def title
-
name
-
end
-
-
def guests
-
User.none
-
end
-
-
def message_channel
-
"/group-#{self.key}"
-
end
-
-
def parent_or_self
-
parent || self
-
end
-
-
def self_and_subgroups
-
Group.where(id: [id].concat(subgroup_ids))
-
end
-
-
def add_member!(user, inviter: nil)
-
save! unless persisted?
-
user.save! unless user.persisted?
-
-
if membership = Membership.find_by(user_id: user.id, group_id: id)
-
if membership.revoked_at
-
membership.update(admin: false, revoked_at: nil, revoker_id: nil, accepted_at: DateTime.now, inviter: inviter)
-
end
-
else
-
membership = Membership.create!(user_id: user.id, group_id: id, inviter: inviter, accepted_at: DateTime.now)
-
end
-
-
GenericWorker.perform_async('PollService', 'group_members_added', self.id)
-
membership
-
rescue ActiveRecord::RecordNotUnique
-
retry
-
end
-
-
def membership_for(user)
-
memberships.find_by(user_id: user.id)
-
end
-
-
def add_members!(users, inviter: nil)
-
users.map { |user| add_member!(user, inviter: inviter) }
-
end
-
-
def add_admin!(user)
-
add_member!(user).tap do |m|
-
m.make_admin!
-
update(creator: user) if creator.blank?
-
end.reload
-
end
-
-
def ensure_handle_is_not_empty
-
self.handle = nil if self.handle.to_s.strip == ""
-
end
-
-
def archive!
-
Group.where(id: id_and_subgroup_ids).update_all(archived_at: DateTime.now)
-
reload
-
end
-
-
def unarchive!
-
Group.where(id: id_and_subgroup_ids).update_all(archived_at: nil)
-
reload
-
end
-
-
def org_members_count
-
Membership.active.where(group_id: id_and_subgroup_ids).count('distinct user_id')
-
end
-
-
def org_accepted_members_count
-
Membership.active.accepted.where(group_id: id_and_subgroup_ids).count('distinct user_id')
-
end
-
-
def org_discussions_count
-
Group.where(id: id_and_subgroup_ids).sum(:discussions_count)
-
end
-
-
def org_polls_count
-
Group.where(id: id_and_subgroup_ids).sum(:polls_count)
-
end
-
-
def is_trial_or_demo?
-
parent_group = parent_or_self
-
subscription = Subscription.for(parent_group)
-
['trial', 'demo'].include?(subscription.plan)
-
end
-
-
def is_subgroup_of_hidden_parent?
-
is_subgroup? and parent.is_hidden_from_public?
-
end
-
-
def is_parent?
-
parent_id.blank?
-
end
-
-
def is_subgroup?
-
!is_parent?
-
end
-
-
def admin_email
-
admins.first.email
-
end
-
-
def full_name
-
if is_subgroup?
-
[parent&.name, name].compact.join(' - ')
-
else
-
name
-
end
-
end
-
-
def id_and_subgroup_ids
-
subgroup_ids.concat([id]).compact.uniq
-
end
-
-
def identity_for(type)
-
group_identities.joins(:identity).find_by("omniauth_identities.identity_type": type)
-
end
-
-
def poll_template_positions
-
self[:info]['poll_template_positions'] ||= {
-
'check' => 1,
-
'advice' => 2,
-
'consent' => 3,
-
'consensus' => 4,
-
'poll' => 5,
-
'score' => 6,
-
'dot_vote' => 7,
-
'ranked_choice' => 8,
-
'meeting' => 9,
-
}
-
self[:info]['poll_template_positions']
-
end
-
-
def categorize_poll_templates
-
if self[:info].has_key? 'categorize_poll_templates'
-
self[:info]['categorize_poll_templates']
-
else
-
true
-
end
-
end
-
-
def category=(val)
-
self[:info]['category'] = val
-
end
-
-
def category
-
self[:info]['category']
-
end
-
-
def categorize_poll_templates=(val)
-
self[:info]['categorize_poll_templates'] = val
-
end
-
-
def hidden_poll_templates
-
self[:info]['hidden_poll_templates'] ||= AppConfig.app_features.fetch(:hidden_poll_templates, [])
-
self[:info]['hidden_poll_templates']
-
end
-
-
def hidden_poll_templates=(val)
-
self[:info]['hidden_poll_templates'] = val
-
end
-
-
def self.ransackable_attributes(auth_object = nil)
-
[
-
"admin_memberships_count",
-
"admin_tags",
-
"admins_can_edit_user_content",
-
"archived_at",
-
"attachments",
-
"category_id",
-
"city",
-
"closed_discussions_count",
-
"closed_motions_count",
-
"closed_polls_count",
-
"cohort_id",
-
"content_locale",
-
"country",
-
"cover_photo_content_type",
-
"cover_photo_file_name",
-
"cover_photo_file_size",
-
"cover_photo_updated_at",
-
"created_at",
-
"creator_id",
-
"default_group_cover_id",
-
"description",
-
"description_format",
-
"discussion_privacy_options",
-
"discussions_count",
-
"full_name",
-
"handle",
-
"id",
-
"invitations_count",
-
"is_referral",
-
"is_visible_to_parent_members",
-
"is_visible_to_public",
-
"key",
-
"listed_in_explore",
-
"logo_content_type",
-
"logo_file_name",
-
"logo_file_size",
-
"logo_updated_at",
-
"members_can_add_guests",
-
"members_can_add_members",
-
"members_can_announce",
-
"members_can_create_subgroups",
-
"members_can_delete_comments",
-
"members_can_edit_comments",
-
"members_can_edit_discussions",
-
"members_can_raise_motions",
-
"members_can_start_discussions",
-
"members_can_vote",
-
"membership_granted_upon",
-
"memberships_count",
-
"name",
-
"new_threads_max_depth",
-
"new_threads_newest_first",
-
"open_discussions_count",
-
"parent_id",
-
"parent_members_can_see_discussions",
-
"pending_memberships_count",
-
"poll_templates_count",
-
"polls_count",
-
"proposal_outcomes_count",
-
"public_discussions_count",
-
"recent_activity_count",
-
"region",
-
"subgroups_count",
-
"subscription_id",
-
"template_discussions_count",
-
"theme_id",
-
"updated_at"]
-
end
-
-
private
-
def variant_path(variant)
-
Rails.application.routes.url_helpers.rails_representation_path(variant, only_path: true)
-
end
-
-
def handle_is_valid
-
self.handle = nil if self.handle.to_s.strip == "" || (is_subgroup? && parent.handle.nil?)
-
return if handle.nil?
-
self.handle = handle.parameterize
-
if is_subgroup? && parent.handle && !handle.starts_with?("#{parent.handle}-")
-
errors.add(:handle, I18n.t(:'group.error.handle_must_begin_with_parent_handle', parent_handle: parent.handle))
-
end
-
end
-
-
def limit_inheritance
-
if parent_id.present?
-
errors[:base] << "Can't set a subgroup as parent" unless parent.parent_id.nil?
-
end
-
end
-
end
-
1
class GroupIdentity < ApplicationRecord
-
1
extend HasCustomFields
-
-
1
attr_writer :make_announcement
-
1
def make_announcement
-
!!@make_announcement
-
end
-
-
1
attr_accessor :webhook_url
-
-
1
belongs_to :group, class_name: 'Group', required: true
-
1
belongs_to :identity, class_name: 'Identities::Base', required: true, dependent: :destroy
-
-
1
set_custom_fields :slack_channel_id, :slack_channel_name
-
-
1
attr_accessor :identity_type
-
-
1
delegate :slack_team_name, to: :identity
-
1
delegate :slack_team_id, to: :identity
-
1
delegate :title, to: :group
-
end
-
class GroupSurvey < ApplicationRecord
-
belongs_to :group, class_name: 'Group', foreign_key: :group_id
-
-
has_one :subscription, through: :group, source: :subscription
-
end
-
class GuestGroup < Group
-
end
-
class Identities::Base < ApplicationRecord
-
extend HasCustomFields
-
self.table_name = :omniauth_identities
-
validates :identity_type, presence: true
-
validates :access_token, presence: true, if: :requires_access_token?
-
validates :uid, presence: true
-
-
belongs_to :user, required: false
-
-
PROVIDERS = YAML.load_file(Rails.root.join("config", "providers.yml"))['identity']
-
discriminate Identities, on: :identity_type
-
scope :with_user, -> { where.not(user: nil) }
-
scope :slack, -> { where(identity_type: :slack) }
-
-
def self.set_identity_type(type)
-
after_initialize { self.identity_type = type }
-
end
-
-
def find_or_create_user!
-
User.find_or_create_by(email: self.email).associate_with_identity(self)
-
end
-
-
def assign_logo!
-
return unless user && logo
-
user.uploaded_avatar.attach(
-
io: URI.open(URI.parse(logo)),
-
filename: File.basename(logo)
-
)
-
user.update(avatar_kind: :uploaded)
-
rescue OpenURI::HTTPError, TypeError
-
# Can't load logo uri as attachment; do nothing
-
end
-
-
def requires_access_token?
-
true
-
end
-
end
-
class Identities::Facebook < Identities::Base
-
include Identities::WithClient
-
set_identity_type :facebook
-
-
def apply_user_info(payload)
-
self.uid ||= payload['id']
-
self.name ||= payload['name']
-
self.email ||= payload['email']
-
end
-
-
def fetch_user_avatar
-
self.logo = client.fetch_user_avatar(self.uid).json
-
end
-
-
def admin_groups
-
if permissions_response.json['error'].blank?
-
client.fetch_admin_groups(self.uid)
-
else
-
permissions_response
-
end
-
end
-
-
private
-
-
def publish_events
-
[]
-
end
-
-
def permissions_response
-
@permission_response ||= client.fetch_permissions(self.uid)
-
end
-
end
-
class Identities::Google < Identities::Base
-
include Identities::WithClient
-
set_identity_type :google
-
-
def apply_user_info(payload)
-
self.uid ||= payload['id']
-
self.name ||= payload['name']
-
self.email ||= payload['email']
-
self.logo ||= payload['picture']
-
end
-
end
-
class Identities::Nextcloud < Identities::Base
-
include Identities::WithClient
-
set_identity_type :nextcloud
-
-
def apply_user_info(payload)
-
payload = payload['ocs']['data']
-
self.uid ||= payload['id']
-
self.name ||= payload['id']
-
self.email ||= payload['email']
-
end
-
end
-
class Identities::Oauth < Identities::Base
-
include Identities::WithClient
-
set_identity_type :oauth
-
-
def apply_user_info(payload)
-
self.uid ||= payload.dig(ENV.fetch('OAUTH_ATTR_UID'))
-
self.name ||= payload.dig(ENV.fetch('OAUTH_ATTR_NAME'))
-
self.email ||= payload.dig(ENV.fetch('OAUTH_ATTR_EMAIL'))
-
end
-
end
-
class Identities::Saml < Identities::Base
-
include Routing
-
set_identity_type :saml
-
attr_accessor :response
-
-
def settings
-
@settings ||= begin
-
if ENV['SAML_IDP_METADATA']
-
settings = OneLogin::RubySaml::IdpMetadataParser.new.parse(ENV['SAML_IDP_METADATA'])
-
else
-
settings = OneLogin::RubySaml::IdpMetadataParser.new.parse_remote(ENV['SAML_IDP_METADATA_URL'])
-
end
-
settings.assertion_consumer_service_url = saml_oauth_url
-
settings.issuer = ENV.fetch('SAML_ISSUER', saml_metadata_url)
-
settings.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
-
settings
-
end
-
end
-
-
def fetch_user_info
-
return unless self.response.is_valid?
-
self.email = self.uid = self.response.nameid
-
self.name = self.response.attributes['displayName']
-
end
-
-
def requires_access_token?
-
false
-
end
-
end
-
1
class LoggedOutUser
-
1
include Null::User
-
1
include AvatarInitials
-
1
attr_accessor :name, :email, :token, :avatar_initials, :locale, :legal_accepted, :recaptcha, :time_zone, :date_time_pref, :autodetect_time_zone
-
-
1
alias :read_attribute_for_serialization :send
-
-
1
def tags
-
Tag.none
-
end
-
-
1
def initialize(name: nil, email: nil, token: nil, locale: I18n.locale, time_zone: 'UTC', date_time_pref: 'day_abbr', params: {}, session: {})
-
3305
@name = name
-
3305
@email = email
-
3305
@token = token
-
3305
@locale = locale
-
3305
@date_time_pref = date_time_pref
-
3305
@time_zone = time_zone
-
3305
@autodetect_time_zone = true
-
3305
@params = params
-
3305
@session = session
-
3305
apply_null_methods!
-
3305
set_avatar_initials if (@name || @email)
-
end
-
-
-
1
def name_or_username
-
@name || @username
-
end
-
-
1
def group_token
-
4
@params[:group_token] || @session[:pending_group_token]
-
end
-
-
1
def membership_token
-
3
@params[:membership_token] || @session[:pending_membership_token]
-
end
-
-
1
def stance_token
-
1
@params[:stance_token]
-
end
-
-
1
def discussion_reader_token
-
10
@params[:discussion_reader_token]
-
end
-
-
1
def create_user
-
User.create(name: name,
-
email: email,
-
token: token,
-
legal_accepted: legal_accepted,
-
require_valid_signup: true,
-
recaptcha: recaptcha)
-
end
-
-
1
def memberships_count
-
0
-
end
-
-
1
def message_channel
-
nil
-
end
-
-
1
def nil_methods
-
3305
super + [:id, :created_at, :avatar_url, :thumb_url, :presence, :restricted, :persisted?, :secret_token, :content_locale, :browseable_group_ids]
-
end
-
-
1
def false_methods
-
3305
super + [:save, :persisted?]
-
end
-
-
1
def errors
-
ActiveModel::Errors.new self
-
end
-
-
1
def email_status
-
User.email_status_for(self.email)
-
end
-
-
1
def avatar_kind
-
'initials'
-
end
-
end
-
1
class LoginToken < ApplicationRecord
-
1
belongs_to :user, required: true
-
1
extend HasTokens
-
-
1
initialized_with_token :token
-
28
initialized_with_token :code, -> { generate_code }
-
-
1
EXPIRATION = ENV.fetch('LOGIN_TOKEN_EXPIRATION_MINUTES', 1440)
-
-
1
scope :unused, -> { where(used: false) }
-
-
1
def useable?
-
8
!used && expires_at > DateTime.now && user.present?
-
end
-
-
1
def expires_at
-
6
self.created_at + EXPIRATION.minutes
-
end
-
-
1
def user
-
46
User.verified.find_by(email: super.email) || super
-
end
-
-
1
def self.generate_code
-
27
code = 0
-
27
while code < 100000
-
31
code = Random.new.rand(999999)
-
end
-
27
code
-
end
-
end
-
1
class MemberEmailAlias < ApplicationRecord
-
1
belongs_to :user
-
1
belongs_to :group
-
1
belongs_to :author, class_name: "User"
-
-
8
scope :blocked, -> { where(user_id: nil) }
-
6
scope :allowed, -> { where.not(user_id: nil) }
-
end
-
class Membership < ApplicationRecord
-
class InvitationAlreadyUsed < StandardError
-
attr_accessor :membership
-
def initialize(obj)
-
self.membership = obj
-
end
-
end
-
-
include CustomCounterCache::Model
-
include HasVolume
-
include HasTimeframe
-
include HasExperiences
-
scope :in_organisation, -> (group) { includes(:user).where(group_id: group.id_and_subgroup_ids) }
-
-
extend FriendlyId
-
extend HasTokens
-
friendly_id :token
-
initialized_with_token :token
-
-
validates_presence_of :group, :user
-
validates_uniqueness_of :user_id, scope: :group_id
-
-
belongs_to :group
-
belongs_to :user
-
belongs_to :inviter, class_name: 'User'
-
belongs_to :revoker, class_name: 'User'
-
has_many :events, as: :eventable, dependent: :destroy
-
-
scope :dangling, -> { joins('left join groups g on memberships.group_id = g.id').where('group_id is not null and g.id is null') }
-
scope :active, -> { where(revoked_at: nil) }
-
scope :pending, -> { active.where(accepted_at: nil) }
-
scope :accepted, -> { where('accepted_at IS NOT NULL') }
-
scope :revoked, -> { where('revoked_at IS NOT NULL') }
-
-
scope :search_for, ->(query) { joins(:user).where("users.name ilike :query or users.username ilike :query or users.email ilike :query", query: "%#{query}%") }
-
-
scope :email_verified, -> { joins(:user).where("users.email_verified": true) }
-
-
scope :for_group, lambda {|group| where(group_id: group)}
-
scope :admin, -> { where(admin: true) }
-
-
has_paper_trail only: [:group_id, :user_id, :inviter_id, :admin, :title, :revoked_at, :revoker_id, :volume, :accepted_at]
-
delegate :name, :email, to: :user, prefix: :user, allow_nil: true
-
delegate :parent, to: :group, prefix: :group, allow_nil: true
-
delegate :name, :full_name, to: :group, prefix: :group
-
delegate :admins, to: :group, prefix: :group
-
delegate :name, to: :inviter, prefix: :inviter, allow_nil: true
-
delegate :mailer, to: :user
-
-
update_counter_cache :group, :memberships_count
-
update_counter_cache :group, :pending_memberships_count
-
update_counter_cache :group, :admin_memberships_count
-
update_counter_cache :user, :memberships_count
-
-
before_create :set_volume
-
-
def author_id
-
inviter_id
-
end
-
-
def author
-
inviter
-
end
-
-
def message_channel
-
"membership-#{token}"
-
end
-
-
def make_admin!
-
update_attribute(:admin, true)
-
end
-
-
def remove_admin!
-
update_attribute(:admin, false)
-
end
-
-
def discussion_readers
-
DiscussionReader.
-
joins(:discussion).
-
where("discussions.group_id": group_id).
-
where("discussion_readers.user_id": user_id)
-
end
-
-
def stances
-
Stance.joins(:poll).
-
where("polls.group_id": group_id).
-
where(participant_id: user_id)
-
end
-
-
private
-
-
def set_volume
-
self.volume = user.default_membership_volume if id.nil?
-
end
-
end
-
1
class MembershipRequest < ApplicationRecord
-
1
include HasEvents
-
-
1
validate :validate_not_in_group_already
-
1
validate :validate_unique_membership_request
-
1
validates_presence_of :responder, if: :response
-
-
1
validates :group, presence: true
-
-
1
validates_length_of :introduction, maximum: 250, unless: :persisted?
-
-
1
belongs_to :group
-
1
belongs_to :requestor, class_name: 'User'
-
1
belongs_to :user, foreign_key: 'requestor_id' # duplicate relationship for eager loading
-
1
belongs_to :responder, class_name: 'User'
-
1
has_many :admins, through: :group
-
-
1
validates :introduction, length: { maximum: Rails.application.secrets.max_message_length }
-
-
1
scope :dangling, -> { joins('left join groups on groups.id = group_id').where('groups.id is null') }
-
2
scope :pending, -> { where(response: nil).order('created_at DESC') }
-
2
scope :responded_to, -> { where('response IS NOT ?', nil).order('responded_at DESC') }
-
1
scope :requested_by, ->(user) { where requestor_id: user.id }
-
-
1
delegate :members, to: :group, prefix: true
-
1
delegate :membership_requests, to: :group, prefix: true
-
1
delegate :members_can_add_members, to: :group, prefix: true
-
1
delegate :name, to: :group, prefix: true
-
1
delegate :mailer, to: :group
-
-
1
delegate :email, to: :requestor, allow_nil: true
-
1
delegate :name, to: :requestor, allow_nil: true
-
-
1
def author_id
-
requestor_id
-
end
-
-
1
def title
-
group.full_name
-
end
-
-
1
def user_id
-
requestor_id
-
end
-
-
1
def approve!(responder)
-
12
set_response_details('approved', responder)
-
end
-
-
1
def ignore!(responder)
-
1
set_response_details('ignored', responder)
-
end
-
-
1
def convert_to_membership!
-
1
group.add_member!(requestor)
-
end
-
-
1
private
-
-
1
def validate_not_in_group_already
-
58
if has_not_been_saved_yet? && already_in_group?
-
3
add_already_in_group_error
-
end
-
end
-
-
1
def validate_unique_membership_request
-
58
if has_not_been_saved_yet? && pending_request_already_exists?
-
3
add_already_requested_membership_error
-
end
-
end
-
-
1
def has_not_been_saved_yet?
-
116
not persisted?
-
end
-
-
1
def already_in_group?
-
46
group_members.exists?(requestor.id)
-
end
-
-
1
def pending_request_already_exists?
-
46
group_membership_requests.where(requestor_id: requestor.id, response: nil).exists?
-
end
-
-
1
def add_already_requested_membership_error
-
3
errors.add(:requestor, I18n.t(:'error.you_have_already_requested_membership'))
-
end
-
-
1
def add_already_in_group_error
-
3
errors.add(:requestor, I18n.t(:'error.you_are_already_a_member_of_this_group'))
-
end
-
-
1
def set_response_details(response, responder)
-
13
self.response = response
-
13
self.responder = responder
-
13
self.responded_at = Time.now
-
13
save!
-
end
-
end
-
1
class Notification < ApplicationRecord
-
1
belongs_to :user
-
1
belongs_to :actor, class_name: "User"
-
1
belongs_to :event
-
-
1
validates_presence_of :user, :event
-
-
1
delegate :eventable, to: :event, allow_nil: true
-
1
delegate :kind, to: :event, allow_nil: true
-
1
delegate :locale, to: :user
-
1
delegate :message_channel, to: :user
-
-
1
scope :dangling, -> { joins('left join events e on notifications.event_id = e.id left join users u on u.id = notifications.user_id').where('e.id is null or u.id is null') }
-
1426
scope :user_mentions, -> { joins(:event).where("events.kind": :user_mentioned) }
-
end
-
class NullDiscussion
-
include Null::Object
-
-
def initialize
-
apply_null_methods!
-
end
-
-
def group
-
self
-
end
-
-
alias :read_attribute_for_serialization :send
-
-
def title
-
"Null discussion"
-
end
-
-
def nil_methods
-
%w(
-
id
-
key
-
presence
-
present?
-
content_locale
-
description
-
description_format
-
group_id
-
message_channel
-
created_at
-
author_id
-
)
-
end
-
-
def true_methods
-
[]
-
end
-
-
def empty_methods
-
[:member_ids]
-
end
-
-
def none_methods
-
{
-
admins: :user,
-
members: :user,
-
memberships: :membership,
-
readers: :user
-
}
-
end
-
end
-
1
class NullGroup
-
1
include Null::Group
-
-
1
def initialize
-
2795
apply_null_methods!
-
end
-
end
-
class NullPoll
-
include Null::Object
-
-
def initialize
-
apply_null_methods!
-
end
-
-
def group
-
self
-
end
-
-
alias :read_attribute_for_serialization :send
-
-
def title
-
"Null poll"
-
end
-
-
def nil_methods
-
%w(
-
id
-
key
-
presence
-
present?
-
content_locale
-
details
-
details_format
-
group_id
-
message_channel
-
created_at
-
author_id
-
)
-
end
-
-
def true_methods
-
[]
-
end
-
-
def empty_methods
-
[:member_ids, :voter_ids]
-
end
-
-
def none_methods
-
{
-
admins: :user,
-
members: :user,
-
memberships: :membership,
-
unmasked_decided_voters: :user,
-
unmasked_undecided_voters: :user,
-
unmasked_voters: :user,
-
non_voters: :user,
-
voters: :user
-
}
-
end
-
end
-
1
class Outcome < ApplicationRecord
-
1
include CustomCounterCache::Model
-
1
extend HasCustomFields
-
1
include HasEvents
-
1
include HasMentions
-
1
include Reactable
-
1
include Translatable
-
1
include HasCreatedEvent
-
1
include HasEvents
-
1
include HasRichText
-
1
include Searchable
-
-
1
def self.pg_search_insert_statement(id: nil, author_id: nil, discussion_id: nil, poll_id: nil)
-
149
content_str = "regexp_replace(CONCAT_WS(' ', outcomes.statement, users.name), E'<[^>]+>', '', 'gi')"
-
149
<<~SQL.squish
-
INSERT INTO pg_search_documents (
-
searchable_type,
-
searchable_id,
-
poll_id,
-
group_id,
-
discussion_id,
-
author_id,
-
authored_at,
-
content,
-
ts_content,
-
created_at,
-
updated_at)
-
SELECT 'Outcome' AS searchable_type,
-
outcomes.id AS searchable_id,
-
outcomes.poll_id AS poll_id,
-
polls.group_id as group_id,
-
polls.discussion_id AS discussion_id,
-
outcomes.author_id AS author_id,
-
outcomes.created_at as authored_at,
-
#{content_str} AS content,
-
to_tsvector('simple', #{content_str}) as ts_content,
-
now() AS created_at,
-
now() AS updated_at
-
FROM outcomes
-
LEFT JOIN users ON users.id = outcomes.author_id
-
LEFT JOIN polls ON polls.id = outcomes.poll_id
-
WHERE polls.discarded_at IS NULL
-
#{id ? " AND outcomes.id = #{id.to_s.to_i} LIMIT 1" : ""}
-
#{author_id ? " AND outcomes.author_id = #{author_id.to_s.to_i}" : ""}
-
#{discussion_id ? " AND polls.discussion_id = #{discussion_id.to_s.to_i}" : ""}
-
#{poll_id ? " AND outcomes.poll_id = #{poll_id.to_s.to_i}" : ""}
-
SQL
-
end
-
1
is_rich_text on: :statement
-
-
1
set_custom_fields :event_summary, :event_description, :event_location
-
-
1936
scope :latest, -> { where(latest: true) }
-
1
scope :dangling, -> { joins('left join polls on polls.id = poll_id').where('polls.id is null') }
-
1
scope :in_organisation, -> (group) { joins(:poll).where('polls.group_id': group.id_and_subgroup_ids) }
-
1
belongs_to :poll, required: true
-
1
belongs_to :poll_option, required: false
-
1
belongs_to :author, class_name: 'User', required: true
-
1
has_many :stances, through: :poll
-
1
has_many :documents, as: :model, dependent: :destroy
-
-
%w(
-
1
title poll_type dates_as_options group group_id discussion discussion_id
-
locale mailer members admins discarded? tags
-
13
).each { |message| delegate message, to: :poll }
-
-
1
is_mentionable on: :statement
-
1
is_translatable on: :statement
-
-
1
has_paper_trail only: [:statement, :statement_format, :author_id, :review_on]
-
2
define_counter_cache(:versions_count) { |d| d.versions.count }
-
1
validates :statement, presence: true, length: { maximum: Rails.application.secrets.max_message_length }
-
1
validate :has_valid_poll_option
-
-
1
scope :review_due_not_published, -> (due_date) do
-
3
where(review_on: due_date).where("NOT EXISTS (
-
SELECT 1 FROM events
-
WHERE events.eventable_id = outcomes.id AND
-
events.eventable_type = 'Outcome' AND
-
events.kind = 'outcome_review_due')")
-
end
-
-
1
def author_name
-
author.name
-
end
-
-
1
def user_id
-
80
author_id
-
end
-
-
1
def body
-
statement
-
end
-
-
1
def body=(val)
-
self.statement = val
-
end
-
-
1
def body_format
-
statement_format
-
end
-
-
1
def parent_event
-
34
poll.created_event
-
end
-
-
1
def attendee_emails
-
8
self.stances.joins(:participant).joins(:stance_choices)
-
.where("stance_choices.poll_option_id": self.poll_option_id)
-
.pluck(:"users.email").flatten.compact.uniq
-
end
-
-
1
def calendar_invite
-
59
return nil unless self.poll_option && self.dates_as_options
-
8
CalendarInvite.new(self).to_ical
-
end
-
-
1
def has_valid_poll_option
-
123
return if !self.poll_option_id || poll.poll_option_ids.include?(self.poll_option_id)
-
1
errors.add(:poll_option_id, I18n.t(:"outcome.error.invalid_poll_option"))
-
end
-
end
-
1
class PermittedParams < Struct.new(:params)
-
MODELS = %w(
-
1
user group membership_request membership poll poll_template outcome
-
stance discussion discussion_template discussion_reader comment
-
contact_message document
-
webhook chatbot contact_request reaction tag
-
)
-
-
1
MODELS.each do |kind|
-
19
define_method(kind) do
-
164
permitted_attributes = self.send("#{kind}_attributes")
-
164
params.require(kind).permit(*permitted_attributes)
-
end
-
19
alias_method :"api_#{kind}", kind.to_sym
-
end
-
-
1
alias :read_attribute_for_serialization :send
-
-
1
def user_attributes
-
3
[:name, :avatar_kind, :email, :password, :password_confirmation, :current_password,
-
:remember_me, :uploaded_avatar, :username, :short_bio, :short_bio_format, :location,
-
:autodetect_time_zone, :time_zone, :selected_locale, :email_when_mentioned, :default_membership_volume,
-
:email_catch_up_day, :has_password, :has_token, :email_status,
-
:email_when_proposal_closing_soon, :email_new_discussions_and_proposals, :email_on_participation, :email_newsletter,
-
:date_time_pref, :bot,
-
:legal_accepted, {email_new_discussions_and_proposals_group_ids: []},
-
:link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
-
]
-
end
-
-
1
def poll_attributes
-
[
-
31
:agree_target,
-
:title,
-
:details,
-
:details_format,
-
:discussion_id,
-
:default_duration_in_days,
-
:poll_type,
-
:group_id,
-
:closing_at,
-
:anonymous,
-
:hide_results,
-
:key,
-
:limit_reason_length,
-
:shuffle_options,
-
:notify_on_closing_soon,
-
:voter_can_add_options,
-
:specified_voters_only,
-
:recipient_audience,
-
:recipient_message,
-
:tags, {tags: []},
-
:notify_recipients,
-
:recipient_user_ids, {recipient_user_ids: []},
-
:recipient_chatbot_ids, {recipient_chatbot_ids: []},
-
:recipient_emails, {recipient_emails: []},
-
:can_respond_maybe,
-
:dots_per_person,
-
:max_score,
-
:min_score,
-
:options, {options: []},
-
:process_name,
-
:process_subtitle,
-
:poll_option_name_format,
-
:reason_prompt,
-
:template,
-
:time_zone,
-
:stance_reason_required,
-
:meeting_duration,
-
:minimum_stance_choices,
-
:maximum_stance_choices,
-
:chart_type,
-
:document_ids, {document_ids: []},
-
:poll_template_id,
-
:poll_template_key,
-
:poll_options_attributes, {poll_options_attributes: [:id, :name, :icon, :meaning, :prompt, :priority, :_destroy]},
-
:link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
-
]
-
end
-
-
1
def poll_template_attributes
-
[
-
:key,
-
:group_id,
-
:position,
-
:author_id,
-
:poll_type,
-
:process_name,
-
:process_subtitle,
-
:process_introduction,
-
:process_introduction_format,
-
:title,
-
:title_placeholder,
-
:details,
-
:details_format,
-
:notify_on_participate,
-
:anonymous,
-
:specified_voters_only,
-
:notify_on_closing_soon,
-
:content_locale,
-
:shuffle_options,
-
:hide_results,
-
:chart_type,
-
:min_score,
-
:max_score,
-
:minimum_stance_choices,
-
:maximum_stance_choices,
-
:dots_per_person,
-
:reason_prompt,
-
:tags, {tags: []},
-
:poll_options, {poll_options: [:name, :icon, :meaning, :prompt, :priority]},
-
:stance_reason_required,
-
:limit_reason_length,
-
:default_duration_in_days,
-
:agree_target,
-
:meeting_duration,
-
:can_respond_maybe,
-
:poll_option_name_format,
-
:outcome_statement,
-
:outcome_statement_format,
-
:outcome_review_due_in_days,
-
:link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
-
]
-
end
-
-
1
def stance_attributes
-
18
[:poll_id, :reason, :reason_format,
-
:stance_choices_attributes, {stance_choices_attributes: [:score, :poll_option_id]},
-
:link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
-
]
-
end
-
-
1
def stance_choice_attributes
-
[:score, :poll_option_id, :stance_id]
-
end
-
-
1
def outcome_attributes
-
23
[:statement, :statement_format, :poll_id, :poll_option_id, :review_on, :recipient_audience, :include_actor,
-
:event_location, :event_summary, :event_description,
-
:notify_recipients,
-
:recipient_user_ids, {recipient_user_ids: []},
-
:recipient_chatbot_ids, {recipient_chatbot_ids: []},
-
:recipient_emails, {recipient_emails: []},
-
:document_ids, {document_ids: []},
-
:link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
-
]
-
end
-
-
1
def membership_request_attributes
-
[:name, :email, :introduction, :group_id]
-
end
-
-
1
def membership_attributes
-
1
[:title, :volume, :apply_to_all, :set_default]
-
end
-
-
1
def discussion_reader_attributes
-
[:discussion_id, :volume]
-
end
-
-
1
def group_attributes
-
5
[:parent_id, :name, :handle, :group_privacy, :is_visible_to_public, :discussion_privacy_options,
-
:members_can_add_members, :members_can_add_guests, :members_can_announce,
-
:members_can_edit_discussions, :members_can_edit_comments, :members_can_delete_comments,
-
:description, :description_format, :is_visible_to_parent_members, :parent_members_can_see_discussions,
-
:membership_granted_upon, :cover_photo, :logo, :category, :members_can_raise_motions,
-
:members_can_start_discussions, :members_can_create_subgroups, :admins_can_edit_user_content,
-
:new_threads_max_depth, :new_threads_newest_first,
-
:document_ids, {document_ids: []},
-
:link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
-
]
-
end
-
-
1
def webhook_attributes
-
[:group_id, :url, :name, :format, :include_body, :include_subgroups, :permissions, :event_kinds, {event_kinds: [], permissions: []}]
-
end
-
-
1
def chatbot_attributes
-
[:name, :group_id, :kind, :webhook_kind, :server, :access_token, :channel, :notification_only, :event_kinds, {event_kinds: []}]
-
end
-
-
1
def discussion_attributes
-
54
[:title,
-
:description,
-
:description_format,
-
:discussion_template_id,
-
:discussion_template_key,
-
:group_id,
-
:newest_first,
-
:max_depth,
-
:private,
-
:notify_recipients,
-
:recipient_audience,
-
:recipient_message,
-
:tags, {tags: []},
-
:recipient_user_ids, {recipient_user_ids: []},
-
:recipient_chatbot_ids, {recipient_chatbot_ids: []},
-
:recipient_emails, {recipient_emails: []},
-
:forked_event_ids, {forked_event_ids: []},
-
:document_ids, {document_ids: []},
-
:link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
-
]
-
end
-
-
1
def discussion_template_attributes
-
[
-
:key,
-
:title,
-
:title_placeholder,
-
:description,
-
:description_format,
-
:process_name,
-
:process_subtitle,
-
:process_introduction,
-
:process_introduction_format,
-
:recipient_audience,
-
:group_id,
-
:newest_first,
-
:max_depth,
-
:public,
-
:poll_template_keys_or_ids, {poll_template_keys_or_ids: []},
-
:tags, {tags: []},
-
:link_previews, :files, :image_files, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]}, {files: []}, {image_files: []}
-
]
-
end
-
-
1
def tag_attributes
-
6
[:name, :color, :group_id, :priority]
-
end
-
-
1
def comment_attributes
-
18
[:body, :body_format, :discussion_id, :parent_id, :parent_type,
-
:document_ids, {document_ids: []},
-
:link_previews, {link_previews: [:image, :title, :description, :url, :hostname, :fit, :align]},
-
:files, {files: []},
-
:image_files, {image_files: []}]
-
end
-
-
1
def reaction_attributes
-
6
[:reaction, :reactable_id, :reactable_type]
-
end
-
-
1
def contact_message_attributes
-
[:email, :subject, :user_id, :message, :name]
-
end
-
-
1
def contact_request_attributes
-
[:recipient_id, :message]
-
end
-
-
1
def document_attributes
-
[:url, :title, :model_id, :model_type, :file, :filename]
-
end
-
end
-
class Poll < ApplicationRecord
-
extend HasCustomFields
-
include CustomCounterCache::Model
-
include ReadableUnguessableUrls
-
include HasEvents
-
include HasMentions
-
include MessageChannel
-
include SelfReferencing
-
include Reactable
-
include HasCreatedEvent
-
include HasRichText
-
include HasTags
-
include Discard::Model
-
include Searchable
-
-
def self.pg_search_insert_statement(id: nil, author_id: nil, discussion_id: nil)
-
content_str = "regexp_replace(CONCAT_WS(' ', polls.title, polls.details, users.name), E'<[^>]+>', '', 'gi')"
-
<<~SQL.squish
-
INSERT INTO pg_search_documents (
-
searchable_type,
-
searchable_id,
-
poll_id,
-
group_id,
-
discussion_id,
-
author_id,
-
authored_at,
-
content,
-
ts_content,
-
created_at,
-
updated_at)
-
SELECT 'Poll' AS searchable_type,
-
polls.id AS searchable_id,
-
polls.id AS poll_id,
-
polls.group_id as group_id,
-
polls.discussion_id AS discussion_id,
-
polls.author_id AS author_id,
-
polls.created_at AS authored_at,
-
#{content_str} AS content,
-
to_tsvector('simple', #{content_str}) as ts_content,
-
now() AS created_at,
-
now() AS updated_at
-
FROM polls
-
LEFT JOIN users ON users.id = polls.author_id
-
WHERE polls.discarded_at IS NULL
-
#{id ? " AND polls.id = #{id.to_i} LIMIT 1" : ""}
-
#{author_id ? " AND polls.author_id = #{author_id.to_i}" : ""}
-
#{discussion_id ? " AND polls.discussion_id = #{discussion_id.to_i}" : ""}
-
SQL
-
end
-
-
is_rich_text on: :details
-
-
extend NoSpam
-
no_spam_for :title, :details
-
-
set_custom_fields :meeting_duration,
-
:time_zone,
-
:can_respond_maybe
-
-
TEMPLATE_DEFAULT_FIELDS = %w[
-
poll_option_name_format
-
max_score
-
min_score
-
dots_per_person
-
chart_type
-
default_duration_in_days
-
]
-
-
TEMPLATE_DEFAULT_FIELDS.each do |field|
-
define_method field, -> {
-
self[field] || self[:custom_fields][field] || AppConfig.poll_types.dig(self.poll_type, 'defaults', field)
-
}
-
-
define_method :"#{field}=", ->(value) {
-
self[:custom_fields].delete(field)
-
if value == AppConfig.poll_types.dig(self.poll_type, 'defaults', field)
-
self[field] = nil
-
else
-
self[field] = value
-
end
-
value
-
}
-
end
-
-
TEMPLATE_VALUES = %w(has_option_icon
-
order_results_by
-
prevent_anonymous
-
vote_method
-
material_icon
-
require_all_choices
-
validate_minimum_stance_choices
-
validate_maximum_stance_choices
-
validate_min_score
-
validate_max_score
-
has_options
-
validate_dots_per_person).freeze
-
-
TEMPLATE_VALUES.each do |field|
-
define_method field, -> { AppConfig.poll_types.dig(self.poll_type, field) }
-
end
-
-
def poll_template
-
return PollTemplate.find_by(id: poll_template_id) if poll_template_id
-
return PollTemplateService.default_templates.find {|pt| pt.key == poll_template_key } if poll_template_key
-
return nil
-
end
-
-
def create_missing_created_event!
-
self.events.create(
-
kind: created_event_kind,
-
user_id: author_id,
-
created_at: created_at,
-
discussion_id: discussion_id)
-
end
-
-
def minimum_stance_choices
-
if require_all_choices
-
poll.poll_options.length
-
else
-
self[:minimum_stance_choices] ||
-
self[:custom_fields][:minimum_stance_choices] ||
-
AppConfig.poll_types.dig(self.poll_type, 'defaults', 'minimum_stance_choices') ||
-
0
-
end
-
end
-
-
def maximum_stance_choices
-
self[:maximum_stance_choices] ||
-
self[:custom_fields][:maximum_stance_choices] ||
-
AppConfig.poll_types.dig(self.poll_type, 'defaults', 'maximum_stance_choices') ||
-
poll.poll_options.length
-
end
-
-
include Translatable
-
is_translatable on: [:title, :details]
-
is_mentionable on: :details
-
-
belongs_to :author, class_name: "User"
-
has_many :outcomes, dependent: :destroy
-
has_one :current_outcome, -> { where(latest: true) }, class_name: 'Outcome'
-
-
belongs_to :discussion
-
belongs_to :group, class_name: "Group"
-
-
enum notify_on_closing_soon: {nobody: 0, author: 1, undecided_voters: 2, voters: 3}
-
enum hide_results: {off: 0, until_vote: 1, until_closed: 2}
-
enum stance_reason_required: {disabled: 0, optional: 1, required: 2}
-
-
has_many :stances, dependent: :destroy
-
has_many :stance_choices, through: :stances
-
has_many :voters, -> { merge(Stance.latest) }, through: :stances, source: :participant
-
has_many :admin_voters, -> { merge(Stance.latest.admin) }, through: :stances, source: :participant
-
has_many :undecided_voters, -> { merge(Stance.latest.undecided) }, through: :stances, source: :participant
-
has_many :decided_voters, -> { merge(Stance.latest.decided) }, through: :stances, source: :participant
-
-
has_many :poll_options, -> { order('priority') }, dependent: :destroy, autosave: true
-
accepts_nested_attributes_for :poll_options, allow_destroy: true
-
-
has_many :documents, as: :model, dependent: :destroy
-
-
scope :dangling, -> { joins('left join groups g on polls.group_id = g.id').where('group_id is not null and g.id is null') }
-
scope :active, -> { kept.where('polls.closed_at': nil) }
-
scope :template, -> { kept.where('polls.template': true) }
-
scope :closed, -> { kept.where("polls.closed_at IS NOT NULL") }
-
scope :recent, -> { kept.where("polls.closed_at IS NULL or polls.closed_at > ?", 7.days.ago) }
-
scope :search_for, ->(fragment) { kept.where("polls.title ilike :fragment", fragment: "%#{fragment}%") }
-
scope :lapsed_but_not_closed, -> { active.where("polls.closing_at < ?", Time.now) }
-
scope :active_or_closed_after, ->(since) { kept.where("polls.closed_at IS NULL OR polls.closed_at > ?", since) }
-
scope :in_organisation, -> (group) { kept.where(group_id: group.id_and_subgroup_ids) }
-
-
scope :closing_soon_not_published, ->(timeframe, recency_threshold = 24.hours.ago) do
-
active
-
.distinct
-
.where(closing_at: timeframe)
-
.where("NOT EXISTS (SELECT 1 FROM events
-
WHERE events.created_at > ? AND
-
events.eventable_id = polls.id AND
-
events.eventable_type = 'Poll' AND
-
events.kind = 'poll_closing_soon')", recency_threshold)
-
end
-
-
validates :poll_type, inclusion: { in: AppConfig.poll_types.keys }
-
validates :details, length: {maximum: Rails.application.secrets.max_message_length }
-
-
before_save :clamp_minimum_stance_choices
-
validate :closes_in_future
-
validate :discussion_group_is_poll_group
-
validate :cannot_deanonymize
-
validate :cannot_reveal_results_early
-
validate :title_if_not_discarded
-
-
alias_method :user, :author
-
-
has_paper_trail only: [
-
:author_id,
-
:title,
-
:details,
-
:details_format,
-
:closing_at,
-
:closed_at,
-
:group_id,
-
:discussion_id,
-
:anonymous,
-
:discarded_at,
-
:discarded_by,
-
:voter_can_add_options,
-
:specified_voters_only,
-
:stance_reason_required,
-
:tags,
-
:notify_on_closing_soon,
-
:poll_option_names,
-
:hide_results]
-
-
update_counter_cache :group, :polls_count
-
update_counter_cache :group, :closed_polls_count
-
update_counter_cache :discussion, :closed_polls_count
-
update_counter_cache :discussion, :anonymous_polls_count
-
-
delegate :locale, to: :author
-
delegate :name, to: :author, prefix: true
-
-
def has_score_icons
-
vote_method == "time_poll"
-
end
-
-
def has_variable_score
-
!(min_score == max_score)
-
end
-
-
def is_single_choice?
-
minimum_stance_choices == 1 && maximum_stance_choices == 1
-
end
-
-
def results_include_undecided
-
poll_type != "meeting"
-
end
-
-
def dates_as_options
-
poll_option_name_format == 'iso8601'
-
end
-
-
def chart_column
-
case poll_type
-
when 'count' then (agree_target ? 'target_percent' : 'voter_percent')
-
when 'check', 'proposal' then 'score_percent'
-
else
-
'max_score_percent'
-
end
-
end
-
-
def can_respond_maybe
-
self[:custom_fields].fetch('can_respond_maybe', false)
-
end
-
-
def result_columns
-
case poll_type
-
when 'proposal'
-
%w[chart name score_percent voter_count voters]
-
when 'check'
-
%w[chart name voter_percent voter_count voters]
-
when 'count'
-
if agree_target
-
%w[chart name target_percent voter_count voters]
-
else
-
%w[chart name voter_count voters]
-
end
-
when 'ranked_choice'
-
%w[chart name rank score_percent score average]
-
when 'dot_vote'
-
%w[chart name score_percent score average voter_count]
-
when 'score'
-
%w[chart name score average voter_count]
-
when 'poll'
-
%w[chart name score_percent voter_count voters]
-
when 'meeting'
-
%w[chart name score voters]
-
else
-
[]
-
end
-
end
-
-
def results
-
PollService.calculate_results(self, self.poll_options)
-
end
-
-
def user_id
-
author_id
-
end
-
-
def existing_member_ids
-
voter_ids
-
end
-
-
def decided_voters_count
-
voters_count - undecided_voters_count
-
end
-
-
def cast_stances_pct
-
return 0 if voters_count == 0
-
((decided_voters_count.to_f / voters_count) * 100).to_i
-
end
-
-
def undecided_voters
-
anonymous? ? User.none : super
-
end
-
-
def decided_voters
-
anonymous? ? User.none : super
-
end
-
-
def unmasked_voters
-
User.where(id: stances.latest.pluck(:participant_id))
-
end
-
-
def unmasked_undecided_voters
-
User.where(id: stances.latest.undecided.pluck(:participant_id))
-
end
-
-
def unmasked_decided_voters
-
User.where(id: stances.latest.decided.pluck(:participant_id))
-
end
-
-
def body
-
details
-
end
-
-
def body=(val)
-
self.details = val
-
end
-
-
def body_format
-
details_format
-
end
-
-
def time_zone
-
custom_fields.fetch('time_zone', author.time_zone)
-
end
-
-
def parent_event
-
if discussion
-
discussion.created_event
-
else
-
nil
-
end
-
end
-
-
def group
-
super || NullGroup.new
-
end
-
-
def show_results?(voted: false)
-
!! case hide_results
-
when 'until_closed'
-
closed_at
-
when 'until_vote'
-
closed_at || voted
-
else
-
true
-
end
-
end
-
-
# this should not be run on anonymous polls
-
def reset_latest_stances!
-
self.transaction do
-
self.stances.update_all(latest: false)
-
Stance.where("id IN
-
(SELECT DISTINCT ON (participant_id) id
-
FROM stances
-
WHERE poll_id = #{id}
-
ORDER BY participant_id, created_at DESC)").update_all(latest: true)
-
end
-
end
-
-
def total_score
-
stance_counts.sum
-
end
-
-
def update_counts!
-
poll_options.reload.each(&:update_counts!)
-
update_columns(
-
stance_counts: poll_options.map(&:total_score), # should rename to option scores
-
voters_count: stances.latest.count, # should rename to stances_count
-
undecided_voters_count: stances.latest.undecided.count,
-
versions_count: versions.count
-
)
-
end
-
-
# people who administer the poll (not necessarily vote)
-
def admins
-
raise "poll.admins only makes sense for persisted polls" if self.new_record?
-
User.active.
-
joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{self.discussion_id || 0} AND dr.user_id = users.id").
-
joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}").
-
joins("LEFT OUTER JOIN stances s ON s.participant_id = users.id AND s.poll_id = #{self.id || 0}").
-
joins("LEFT OUTER JOIN polls p ON p.author_id = users.id AND p.id = #{self.id || 0}").
-
where("(m.id IS NOT NULL AND m.revoked_at IS NULL AND m.admin = TRUE) OR /* group admin */
-
(p.author_id = users.id AND p.group_id IS NOT NULL AND m.id IS NOT NULL) OR /* poll author and group member */
-
(p.author_id = users.id AND p.group_id IS NULL) OR /* poll author and no group */
-
(p.author_id = users.id AND dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.guest = TRUE) OR /* poll author and discussion guest */
-
(dr.id IS NOT NULL AND m.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.admin = TRUE) OR /* discussion admin, group member */
-
(dr.id IS NOT NULL AND m.id IS NULL AND dr.revoked_at IS NULL AND dr.admin = TRUE AND dr.guest = TRUE) OR /* discussion guest admin, not group member */
-
(s.id IS NOT NULL AND m.id IS NOT NULL AND s.revoked_at IS NULL AND latest = TRUE AND s.admin = TRUE) OR /* poll admin, group member */
-
(s.id IS NOT NULL AND m.id IS NULL AND s.revoked_at IS NULL AND latest = TRUE AND s.admin = TRUE AND s.guest = TRUE /* poll admin guest */)")
-
end
-
-
# people who can read the poll, not necessarily vote
-
def members
-
User.active.
-
joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{self.discussion_id || 0} AND dr.user_id = users.id").
-
joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}").
-
joins("LEFT OUTER JOIN stances s ON s.participant_id = users.id AND s.poll_id = #{self.id || 0}").
-
where("(dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.guest = TRUE) OR
-
(m.id IS NOT NULL AND m.revoked_at IS NULL) OR
-
(s.id IS NOT NULL AND s.revoked_at IS NULL AND s.guest = TRUE AND latest = TRUE)")
-
end
-
-
def add_guest!(user, author)
-
stances.create!(participant_id: user.id, inviter: author, guest: true, volume: DiscussionReader.volumes[:normal])
-
end
-
-
def add_admin!(user, author)
-
stances.create!(participant_id: user.id, inviter: author, volume: DiscussionReader.volumes[:normal], admin: true)
-
end
-
-
def active?
-
(closing_at && closing_at > Time.now) && !closed_at
-
end
-
-
def wip?
-
closing_at.nil?
-
end
-
-
def closed?
-
!!closed_at
-
end
-
-
def poll_option_names
-
poll_options.map(&:name)
-
end
-
-
def poll_option_names=(names)
-
names = Array(names)
-
existing = Array(poll_options.pluck(:name))
-
names = names.sort if poll_type == 'meeting'
-
names.each_with_index do |name, priority|
-
option = poll_options.find_or_initialize_by(name: name)
-
option.priority = priority
-
os = AppConfig.poll_types.dig(self.poll_type, 'common_poll_options') || []
-
if params = os.find {|o| o['key'] == name }
-
option.name = I18n.t(params['name_i18n'])
-
option.icon = params['icon']
-
option.meaning = I18n.t(params['meaning_i18n'])
-
option.prompt = I18n.t(params['prompt_i18n'])
-
end
-
end
-
removed = (existing - names)
-
poll_options.each {|option| option.mark_for_destruction if removed.include?(option.name) }
-
names
-
end
-
-
alias options= poll_option_names=
-
alias options poll_option_names
-
-
def is_new_version?
-
!self.poll_options.map(&:persisted?).all? ||
-
(['title', 'details', 'closing_at'] & self.changes.keys).any?
-
end
-
-
def discussion_id=(discussion_id)
-
super.tap { self.group_id = self.discussion&.group_id }
-
end
-
-
def discussion=(discussion)
-
super.tap { self.group_id = self.discussion&.group_id }
-
end
-
-
def prioritise_poll_options!
-
if self.poll_type == 'meeting'
-
self.poll_options.sort {|a,b| a.name <=> b.name }.each_with_index {|o, i| o.priority = i }
-
end
-
end
-
-
private
-
-
def title_if_not_discarded
-
if !discarded_at && title.to_s.empty?
-
errors.add(:title, I18n.t(:"activerecord.errors.messages.blank"))
-
end
-
end
-
-
def cannot_deanonymize
-
if anonymous_changed? && anonymous_was == true
-
errors.add :anonymous, :cannot_deanonymize
-
end
-
end
-
-
def cannot_reveal_results_early
-
if hide_results_changed? && (hide_results_was == 'until_closed')
-
errors.add :hide_results, :cannot_show_results_early
-
end
-
end
-
-
def closes_in_future
-
return if closed_at
-
return if closing_at.nil?
-
return if closing_at > Time.zone.now
-
errors.add(:closing_at, I18n.t(:"poll.error.must_be_in_the_future"))
-
end
-
-
def discussion_group_is_poll_group
-
return if poll.group.nil?
-
return if poll.discussion.nil?
-
poll.group_id = poll.discussion.group_id if poll.group_id.nil? && poll.discussion.group_id
-
return if poll.discussion.group_id == poll.group_id
-
self.errors.add(:group, 'Poll group is not discussion group')
-
end
-
-
def clamp_minimum_stance_choices
-
return if minimum_stance_choices.nil?
-
if minimum_stance_choices > poll_options.length
-
self.minimum_stance_choices = poll_options.length
-
end
-
end
-
end
-
1
class PollOption < ApplicationRecord
-
1
include Translatable
-
-
1
belongs_to :poll
-
1
validates :name, presence: true
-
-
1
has_many :stance_choices, dependent: :destroy
-
9619
has_many :stances, -> { where("stances.revoked_at IS NULL") }, through: :stance_choices
-
-
1
is_translatable on: [:name, :meaning]
-
-
1
scope :dangling, -> { joins('left join polls on polls.id = poll_id').where('polls.id is null') }
-
-
1
def update_counts!
-
4809
update_columns(
-
8268
voter_scores: poll.anonymous ? {} : stance_choices.latest.where('stances.participant_id is not null').includes(:stance).map { |c| [c.stance.participant_id, c.score] }.to_h,
-
total_score: stance_choices.latest.sum(:score),
-
voter_count: stances.latest.count
-
)
-
end
-
-
1
def icon
-
8192
self[:icon] || {
-
agree: 'agree',
-
disagree: 'disagree',
-
abstain: 'abstain',
-
block: 'block',
-
consent: 'agree',
-
objection: 'disagree',
-
yes: 'agree',
-
no: 'disagree'
-
}[name.to_sym]
-
end
-
-
1
def color
-
9659
if poll.vote_method == 'show_thumbs'
-
{
-
2560
'agree' => AppConfig.colors['proposal'][0],
-
'abstain' => AppConfig.colors['proposal'][1],
-
'disagree' => AppConfig.colors['proposal'][2],
-
'block' => AppConfig.colors['proposal'][3],
-
}.fetch(icon, AppConfig.colors['proposal'][0])
-
else
-
7099
AppConfig.colors.dig('poll', self.priority % AppConfig.colors.length)
-
end
-
end
-
-
1
def voter_ids
-
# this is a hack, we both know this
-
# some polls 0 is a vote, others it is not
-
1870
if poll.poll_type == 'meeting'
-
86
voter_scores.keys.map(&:to_i)
-
else
-
2991
voter_scores.filter{|id, score| score != 0 }.keys.map(&:to_i)
-
end
-
end
-
-
1
def average_score
-
1870
return 0 if voter_count == 0
-
987
(total_score.to_f / voter_count.to_f)
-
end
-
end
-
1
class PollTemplate < ApplicationRecord
-
1
include Discard::Model
-
1
include HasRichText
-
1
include CustomCounterCache::Model
-
-
1
is_rich_text on: :details
-
-
1
belongs_to :author, class_name: "User"
-
1
belongs_to :group, class_name: "Group"
-
-
1
enum notify_on_closing_soon: {nobody: 0, author: 1, undecided_voters: 2, voters: 3}
-
1
enum hide_results: {off: 0, until_vote: 1, until_closed: 2}
-
1
enum stance_reason_required: {disabled: 0, optional: 1, required: 2}
-
-
1
update_counter_cache :group, :poll_templates_count
-
-
1
validates :poll_type, inclusion: { in: AppConfig.poll_types.keys }
-
1
validates :details, length: { maximum: Rails.application.secrets.max_message_length }
-
1
validates :process_name, presence: true
-
1
validates :process_subtitle, presence: true
-
1
validates :default_duration_in_days, presence: true
-
-
1
has_paper_trail only: [
-
:poll_type,
-
:process_name,
-
:process_subtitle,
-
:process_introduction,
-
:process_introduction_format,
-
:title,
-
:details,
-
:details_format,
-
:group_id,
-
:anonymous,
-
:shuffle_options,
-
:chart_type,
-
:specified_voters_only,
-
:stance_reason_required,
-
:notify_on_closing_soon,
-
:hide_results,
-
:min_score,
-
:max_score,
-
:minimum_stance_choices,
-
:maximum_stance_choices,
-
:dots_per_person,
-
:reason_prompt,
-
:poll_options,
-
:limit_reason_length,
-
:default_duration_in_days,
-
:meeting_duration,
-
:can_respond_maybe,
-
:tags,
-
:discarded_at
-
]
-
-
1
def dump_i18n
-
out = {}
-
[
-
:title,
-
:title_placeholder,
-
:process_name,
-
:process_subtitle,
-
:process_introduction,
-
:details,
-
:reason_prompt,
-
].map(&:to_s).each do |key|
-
unless self.send(key) == AppConfig.poll_types.dig(self.poll_type, 'defaults', key)
-
out[key] = self[key]
-
end
-
end
-
-
tags.each do |tag|
-
out[tag.underscore.gsub(" ", "_")] = tag
-
end
-
-
self.poll_options.each do |poll_option|
-
option_name = poll_option.slice('name').values[0].parameterize(separator: '_').gsub('-', '_')
-
poll_option.slice('name', 'meaning', 'prompt').each_pair do |key, value|
-
if key == 'name'
-
out[option_name] = value
-
else
-
out[option_name+"_"+key] = value
-
end
-
end
-
end
-
-
{process_name.underscore.gsub(" ", "_") => out}
-
end
-
end
-
1
class Reaction < ApplicationRecord
-
1
belongs_to :reactable, polymorphic: true
-
1
belongs_to :user
-
-
# TODO: ensure one reaction per reactable
-
# validates_uniqueness_of :user_id, scope: :reactable
-
1
validates_presence_of :user, :reactable
-
-
1
delegate :group, to: :reactable, allow_nil: true
-
1
delegate :group_id, to: :reactable, allow_nil: true
-
1
delegate :members, to: :reactable, allow_nil: true
-
-
1
alias :author :user
-
1
def author_id
-
2
user_id
-
end
-
-
1
def message_channel
-
case reactable
-
when Outcome, Stance, Poll then reactable.poll.message_channel
-
when Comment, Discussion then reactable.discussion.message_channel
-
end
-
end
-
end
-
1
class ReceivedEmail < ApplicationRecord
-
1
has_many_attached :attachments
-
1
belongs_to :group
-
-
6
scope :unreleased, -> { where(released: false) }
-
1
scope :released, -> { where(released: true) }
-
-
1
def header(name)
-
1257
headers.find { |key, value| key.downcase == name.to_s.downcase }&.last
-
end
-
-
1
def recipient_emails
-
73
String(header('to')).scan(AppConfig::EMAIL_REGEX).uniq
-
end
-
-
1
def route_address
-
73
reply_hostnames = [ENV['REPLY_HOSTNAME'], ENV['OLD_REPLY_HOSTNAME']].compact
-
73
recipient_emails.find do |email|
-
73
reply_hostnames.include? email.split('@')[1].downcase
-
end
-
end
-
-
1
def route_path
-
46
route_address.split('@')[0]
-
end
-
-
1
def sender_hostname
-
27
sender_email.split('@')[1]
-
end
-
-
1
def sender_email
-
54
String(header('from')).scan(AppConfig::EMAIL_REGEX).uniq.first
-
end
-
-
1
def sender_name
-
19
full_address = header('from').strip
-
19
name = full_address.split('<').first.strip.delete('"')
-
19
if name.present? && name != full_address
-
12
name
-
else
-
nil
-
end
-
end
-
-
1
def from
-
1
header('from').strip
-
end
-
-
1
def sender_name_and_email
-
8
if sender_name
-
4
"\"#{sender_name}\" <#{sender_email}>"
-
else
-
4
sender_email
-
end
-
end
-
-
1
def body_format
-
5
if body_html.present?
-
5
'html'
-
else
-
'md'
-
end
-
end
-
-
1
def full_body
-
5
self.body_html.presence || self.body_text
-
end
-
-
1
def reply_body
-
3
text = if body_html.present?
-
3
Premailer.new(body_html, line_length: 10000, with_html_string: true).to_plain_text
-
else
-
body_text
-
end
-
-
3
ReceivedEmailService.extract_reply_body(text, sender_name)
-
end
-
-
1
def subject
-
191
String(header('subject')).gsub(/^( *(re|fwd?)(:| ) *)+/i, '')
-
end
-
-
1
def title
-
8
sender_name_and_email
-
end
-
-
1
def is_addressed_to_loomio?
-
13
route_address.present?
-
end
-
-
1
def is_auto_response?
-
13
return true if header('X-Autorespond')
-
13
return true if header('X-Precedence') == 'auto_reply'
-
-
prefixes = [
-
13
'Auto:',
-
'Automatic reply',
-
'Autosvar',
-
'Automatisk svar',
-
'Automatisch antwoord',
-
'Abwesenheitsnotiz',
-
'Risposta Non al computer',
-
'Automatisch antwoord',
-
'Auto Response',
-
'Respuesta automática',
-
'Fuori sede',
-
'Out of Office',
-
'Frånvaro',
-
'Réponse automatique'
-
]
-
-
195
prefixes.any? { |prefix| subject.downcase.starts_with?(prefix.downcase) }
-
end
-
end
-
1
class SearchResult
-
1
include ActiveModel::Model
-
1
include ActiveModel::Serialization
-
-
1
attr_accessor :id,
-
:searchable_type,
-
:searchable_id,
-
:poll_title,
-
:discussion_title,
-
:discussion_key,
-
:highlight,
-
:poll_key,
-
:poll_id,
-
:sequence_id,
-
:group_handle,
-
:group_key,
-
:group_id,
-
:group_name,
-
:author_name,
-
:author_id,
-
:authored_at,
-
:tags
-
-
1
def poll
-
Poll.find_by(id: poll_id)
-
end
-
-
1
def author
-
User.find_by(id: author_id)
-
end
-
end
-
class SiteSettings
-
def self.colors
-
{
-
primary: "#E3E4E6",
-
agree: "#94D587",
-
abstain: "#EEBC57",
-
disagree: "#D1908F",
-
block: "#D80D00"
-
}.with_indifferent_access
-
end
-
end
-
1
class Stance < ApplicationRecord
-
1
include CustomCounterCache::Model
-
1
include HasMentions
-
1
include Reactable
-
1
include HasEvents
-
1
include HasCreatedEvent
-
1
include HasVolume
-
1
include Searchable
-
-
1
extend HasTokens
-
1
initialized_with_token :token
-
-
1
def self.pg_search_insert_statement(id: nil, author_id: nil, discussion_id: nil, poll_id: nil)
-
634
content_str = "regexp_replace(CONCAT_WS(' ', stances.reason, users.name), E'<[^>]+>', '', 'gi')"
-
634
<<~SQL.squish
-
INSERT INTO pg_search_documents (
-
searchable_type,
-
searchable_id,
-
poll_id,
-
group_id,
-
discussion_id,
-
author_id,
-
authored_at,
-
content,
-
ts_content,
-
created_at,
-
updated_at)
-
SELECT 'Stance' AS searchable_type,
-
stances.id AS searchable_id,
-
stances.poll_id AS poll_id,
-
polls.group_id as group_id,
-
polls.discussion_id AS discussion_id,
-
stances.participant_id AS author_id,
-
stances.cast_at AS authored_at,
-
#{content_str} AS content,
-
to_tsvector('simple', #{content_str}) as ts_content,
-
now() AS created_at,
-
now() AS updated_at
-
FROM stances
-
LEFT JOIN users ON users.id = stances.participant_id
-
LEFT JOIN polls ON polls.id = stances.poll_id
-
WHERE polls.discarded_at IS NULL
-
AND stances.cast_at IS NOT null
-
AND NOT (polls.anonymous = TRUE AND polls.closed_at IS NULL)
-
AND NOT (polls.hide_results = 2 AND polls.closed_at IS NULL)
-
#{id ? " AND stances.id = #{id.to_i} LIMIT 1" : ''}
-
#{author_id ? " AND stances.participant_id = #{author_id.to_i}" : ''}
-
#{discussion_id ? " AND polls.discussion_id = #{discussion_id.to_i}" : ''}
-
#{poll_id ? " AND stances.poll_id = #{poll_id.to_i}" : ''}
-
SQL
-
end
-
-
1
ORDER_SCOPES = ['newest_first', 'oldest_first', 'priority_first', 'priority_last']
-
1
include Translatable
-
1
is_translatable on: :reason
-
1
is_mentionable on: :reason
-
1
include HasRichText
-
-
1
is_rich_text on: :reason
-
-
1
belongs_to :poll, required: true
-
1
belongs_to :inviter, class_name: 'User'
-
-
1
has_many :stance_choices, dependent: :destroy
-
1
has_many :poll_options, through: :stance_choices
-
-
1
has_paper_trail only: [:reason, :option_scores, :revoked_at, :revoker_id, :inviter_id]
-
-
1
accepts_nested_attributes_for :stance_choices
-
-
1
belongs_to :participant, class_name: 'User', required: true
-
-
1
alias :user :participant
-
1
alias :author :participant
-
-
1
scope :dangling, -> { joins('left join polls on polls.id = poll_id').where('polls.id is null') }
-
21336
scope :latest, -> { where(latest: true, revoked_at: nil) }
-
1
scope :guests, -> { where(guest: true) }
-
1
scope :admins, -> { where(admin: true) }
-
1
scope :newest_first, -> { order("cast_at DESC NULLS LAST") }
-
1
scope :undecided_first, -> { order("cast_at DESC NULLS FIRST") }
-
1
scope :oldest_first, -> { order(created_at: :asc) }
-
1
scope :priority_first, -> { joins(:poll_options).order('poll_options.priority ASC') }
-
1
scope :priority_last, -> { joins(:poll_options).order('poll_options.priority DESC') }
-
291
scope :with_reason, -> { where("reason IS NOT NULL AND reason != '' AND reason != '<p></p>'") }
-
1
scope :in_organisation, ->(group) { joins(:poll).where("polls.group_id": group.id_and_subgroup_ids) }
-
47
scope :decided, -> { where("stances.cast_at IS NOT NULL") }
-
2323
scope :undecided, -> { where("stances.cast_at IS NULL") }
-
1270
scope :revoked, -> { where("revoked_at IS NOT NULL") }
-
21
scope :guests, -> { where("inviter_id is not null") }
-
-
3
scope :redeemable, -> { latest.guests.undecided.where('stances.accepted_at IS NULL') }
-
1
scope :redeemable_by, -> (user_id) {
-
2
redeemable.joins(:participant).where("stances.participant_id = ? or users.email_verified = false", user_id)
-
}
-
-
1
validate :valid_minimum_stance_choices
-
1
validate :valid_maximum_stance_choices
-
1
validate :valid_dots_per_person
-
1
validate :valid_reason_length
-
1
validate :valid_reason_required
-
1
validate :valid_require_all_choices
-
-
1
%w(group mailer group_id discussion_id discussion members voters title tags).each do |message|
-
9
delegate(message, to: :poll)
-
end
-
-
1
alias :author :participant
-
-
1
before_save :assign_option_scores
-
1
after_save :update_versions_count!
-
-
1
def build_replacement
-
53
Stance.new(
-
poll_id: self.poll_id,
-
participant_id: self.participant_id,
-
inviter_id: self.inviter_id,
-
reason_format: self.reason_format,
-
latest: true
-
)
-
end
-
-
1
def create_missing_created_event!
-
310
self.events.create(
-
kind: created_event_kind,
-
310
user_id: (poll.anonymous? ? nil: author_id),
-
created_at: created_at,
-
310
discussion_id: (add_to_discussion? ? poll.discussion_id : nil)
-
)
-
end
-
-
1
def author_name
-
3
participant&.name
-
end
-
-
1
def assign_option_scores
-
580
self.option_scores = build_option_scores
-
end
-
-
1
def build_option_scores
-
1485
stance_choices.map { |sc| [sc.poll_option_id.to_s, sc.score] }.to_h
-
end
-
-
1
def update_option_scores!
-
1
update_columns(option_scores: assign_option_scores)
-
end
-
-
1
def update_versions_count!
-
578
update_columns(versions_count: versions.count)
-
end
-
-
1
def author_id
-
245
participant_id
-
end
-
-
1
def user_id
-
80
participant_id
-
end
-
-
1
def locale
-
author&.locale || group&.locale || poll.author.locale
-
end
-
-
1
def add_to_discussion?
-
378
poll.discussion_id &&
-
poll.hide_results != 'until_closed' &&
-
!body_is_blank? &&
-
!Event.where(eventable: self,
-
discussion_id: poll.discussion_id,
-
kind: ['stance_created', 'stance_updated']).exists?
-
end
-
-
1
def body
-
reason
-
end
-
-
1
def body_format
-
reason_format
-
end
-
-
1
def parent_event
-
259
poll.created_event
-
end
-
-
1
def discarded?
-
33
false
-
end
-
-
1
def choice=(choice)
-
445
self.cast_at ||= Time.zone.now
-
445
if choice.kind_of?(Hash)
-
398
self.stance_choices_attributes = poll.poll_options.where(name: choice.keys).map do |option|
-
917
{poll_option_id: option.id,
-
score: choice[option.name]}
-
end
-
else
-
47
options = poll.poll_options.where(name: choice)
-
47
self.stance_choices_attributes = options.map do |option|
-
48
{poll_option_id: option.id}
-
end
-
end
-
end
-
-
1
def participant(bypass = false)
-
2552
super() if bypass
-
2552
(!participant_id || poll.anonymous?) ? AnonymousUser.new : super()
-
end
-
-
1
def real_participant
-
104
User.find_by(id: participant_id)
-
end
-
-
1
def score_for(option)
-
option_scores[option.id] || 0
-
end
-
-
1
private
-
-
1
def valid_min_score
-
return if !cast_at
-
return unless poll.validate_min_score
-
return if (stance_choices.map(&:score).min || 0) >= poll.min_score
-
errors.add(:stance_choices, "min_score validation failure")
-
end
-
-
1
def valid_max_score
-
return if !cast_at
-
return unless poll.validate_max_score
-
return if (stance_choices.map(&:score).max) <= poll.max_score
-
errors.add(:stance_choices, "max_score validation failure")
-
end
-
-
1
def valid_dots_per_person
-
2574
return if !cast_at
-
437
return unless poll.validate_dots_per_person
-
153
return if stance_choices.map(&:score).sum <= poll.dots_per_person.to_i
-
errors.add(:dots_per_person, "Too many dots")
-
end
-
-
1
def valid_minimum_stance_choices
-
2574
return if !cast_at
-
437
return unless poll.validate_minimum_stance_choices
-
278
return if stance_choices.length >= poll.minimum_stance_choices
-
5
errors.add(:stance_choices, "too few stance choices")
-
end
-
-
1
def valid_maximum_stance_choices
-
2574
return if !cast_at
-
437
return unless poll.validate_maximum_stance_choices
-
178
return if stance_choices.length <= poll.maximum_stance_choices
-
errors.add(:stance_choices, "too many stance choices")
-
end
-
-
1
def valid_require_all_choices
-
2574
return if !cast_at
-
437
return unless poll.require_all_choices
-
112
return if poll.poll_options.length == 0
-
112
return if stance_choices.length == poll.poll_options.length
-
3
errors.add(:stance_choices, "require_all_stance_choices")
-
end
-
-
1
def valid_reason_length
-
2574
return if !cast_at
-
437
return if !poll.limit_reason_length
-
437
return if reason_visible_text.length < 501
-
1
errors.add(:reason, I18n.t(:"poll_common.too_long"))
-
end
-
-
1
def valid_reason_required
-
2574
return if !cast_at
-
437
return if poll.stance_reason_required != "required"
-
return if reason_visible_text.length > 5
-
errors.add(:reason, I18n.t(:"poll_common_form.stance_reason_is_required"))
-
end
-
end
-
1
class StanceChoice < ApplicationRecord
-
1
belongs_to :poll_option
-
1
belongs_to :stance
-
1
has_one :poll, through: :poll_option
-
1
delegate :has_variable_score, to: :poll, allow_nil: true
-
-
1
validates_presence_of :poll_option
-
1
validate :total_score_is_valid
-
912
validates :score, numericality: { equal_to: 1 }, if: Proc.new { |sc| sc.stance && !sc.stance.cast_at && sc.poll && !sc.has_variable_score }
-
-
1
scope :dangling, -> { joins('left join stances on stances.id = stance_id').where('stances.id': nil) }
-
8340
scope :latest, -> { joins(:stance).where('stances.latest': true).where('stances.revoked_at': nil) }
-
1
scope :reasons_first, -> {
-
joins(:stance).order(Arel.sql("CASE coalesce(stances.reason, '') WHEN '' THEN 1 ELSE 0 END"))
-
.order(:created_at)
-
}
-
-
1
def rank
-
self.poll.minimum_stance_choices - self.score + 1 if poll.poll_type == 'ranked_choice'
-
end
-
-
1
def rank_or_score
-
rank || score
-
end
-
-
1
private
-
1
def total_score_is_valid
-
911
return unless poll # when we are cloning records and poll is not saved yet
-
-
901
if poll.custom_fields['min_score'] && score < poll.custom_fields['min_score'].to_i
-
errors.add(:score, "Score lower than permitted min")
-
end
-
-
901
if poll.custom_fields['max_score'] && score > poll.custom_fields['max_score'].to_i
-
errors.add(:score, "Score higher than permitted max")
-
end
-
end
-
end
-
class Subscription < ApplicationRecord
-
class MaxMembersExceeded < StandardError; end
-
class NotActive < StandardError; end
-
include SubscriptionConcern if Object.const_defined?('SubscriptionConcern')
-
-
PAYMENT_METHODS = ["chargify", "manual", "barter", "paypal"]
-
ACTIVE_STATES = %w[active on_hold pending]
-
-
scope :dangling, -> { joins('LEFT JOIN groups ON subscriptions.id = groups.subscription_id').where('groups.id IS NULL') }
-
scope :active, -> { where(state: ACTIVE_STATES).where("expires_at is null OR expires_at > ?", Time.current) }
-
scope :expired, -> { where(state: ACTIVE_STATES).where("expires_at < ?", Time.current) }
-
scope :canceled, -> { where(state: :canceled) }
-
-
has_many :groups
-
belongs_to :owner, class_name: 'User'
-
-
attr_accessor :chargify_product_id
-
-
has_paper_trail
-
-
def self.for(group)
-
parent = group.parent_or_self
-
parent.subscription || begin
-
parent.subscription = Subscription.new
-
parent.save
-
parent.subscription
-
end
-
end
-
-
def can_invite()
-
parent_group = parent_or_self
-
subscription = Subscription.for(parent_group)
-
subscription.max_members && parent_group.org_members_count >= subscription.max_members
-
end
-
-
def level
-
SubscriptionService::PLANS[self.plan][:level]
-
end
-
-
def config
-
SubscriptionService::PLANS[Subscription.last.plan.to_sym]
-
end
-
-
def is_active?
-
ACTIVE_STATES.include?(state) && (self.expires_at.nil? || self.expires_at > Time.current)
-
end
-
-
def management_link
-
(self.info || {})['chargify_management_link']
-
end
-
-
def self.ransackable_associations(auth_object = nil)
-
["groups", "owner", "versions"]
-
end
-
-
def self.ransackable_attributes(auth_object = nil)
-
["activated_at",
-
"canceled_at",
-
"chargify_subscription_id",
-
"created_at",
-
"expires_at",
-
"id",
-
"info",
-
"max_members",
-
"max_orgs",
-
"max_threads",
-
"members_count",
-
"owner_id",
-
"payment_method",
-
"plan",
-
"renewed_at",
-
"renews_at",
-
"state",
-
"updated_at"]
-
end
-
end
-
class Tag < ApplicationRecord
-
COLORS = %w[#f44336
-
#e91e63
-
#9c27b0
-
#673ab7
-
#3f51b5
-
#2196f3
-
#03a9f4
-
#00bcd4
-
#009688
-
#4caf50
-
#8bc34a
-
#ffeb3b
-
#ffc107
-
#ff9800
-
#ff5722
-
#795548
-
#607d8b
-
#9e9e9e]
-
-
include Translatable
-
is_translatable on: :name
-
belongs_to :group
-
-
validates :name, presence: true, uniqueness: { scope: :group }
-
# validates :color, presence: true, format: /\A#([A-F0-9]{3}){1,2}\z/i
-
before_validation :set_defaults
-
-
private
-
def set_defaults
-
colors = ['#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#03a9f4', '#00bcd4', '#009688', '#4caf50', '#8bc34a', '#cddc39', '#ffeb3b', '#ffc107', '#ff9800', '#ff5722', '#795548', '#607d8b', '#9e9e9e']
-
self.color = colors.sample if color.blank?
-
end
-
end
-
class Tagging < ActiveRecord::Base
-
include CustomCounterCache::Model
-
-
belongs_to :taggable, polymorphic: true
-
belongs_to :tag
-
-
update_counter_cache :tag, :taggings_count
-
end
-
1
class Task < ApplicationRecord
-
1
include Discard::Model
-
-
1
belongs_to :record, polymorphic: true
-
1
belongs_to :author, class_name: 'User'
-
1
belongs_to :doer, class_name: 'User'
-
-
2
scope :not_done, -> { where(done: false) }
-
-
1
has_many :tasks_users
-
1
has_many :users, through: :tasks_users
-
end
-
1
class TasksUser < ApplicationRecord
-
1
belongs_to :task
-
1
belongs_to :user
-
end
-
class Translation < ApplicationRecord
-
belongs_to :translatable, polymorphic: true
-
scope :to_language, ->(language) { where(language: language) }
-
-
validates_presence_of :translatable, :language
-
# validates :fields, presence: true
-
-
# TODO: Should probably move to a serializer
-
def as_json
-
{ id: translatable_id }.merge(fields)
-
end
-
end
-
class User < ApplicationRecord
-
include CustomCounterCache::Model
-
include ReadableUnguessableUrls
-
include MessageChannel
-
include HasExperiences
-
include HasAvatar
-
include SelfReferencing
-
include NoForbiddenEmails
-
include CustomCounterCache::Model
-
include HasRichText
-
include LocalesHelper
-
-
is_rich_text on: :short_bio
-
-
extend HasTokens
-
extend HasDefaults
-
-
extend NoSpam
-
no_spam_for :name, :email
-
-
has_paper_trail only: [:name, :username, :email, :email_newsletter, :deactivated_at, :deactivator_id]
-
-
MAX_AVATAR_IMAGE_SIZE_CONST = 100.megabytes
-
-
devise :database_authenticatable, :recoverable, :registerable, :rememberable, :lockable, :trackable
-
devise :pwned_password if Rails.env.production?
-
attr_accessor :recaptcha
-
attr_accessor :restricted
-
attr_accessor :token
-
attr_accessor :membership_token
-
attr_accessor :group_token
-
attr_accessor :discussion_reader_token
-
attr_accessor :stance_token
-
-
attr_accessor :legal_accepted
-
-
attr_writer :has_password
-
attr_accessor :require_valid_signup
-
attr_accessor :require_recaptcha
-
-
before_save :set_legal_accepted_at, if: :legal_accepted
-
-
validates :email, presence: true, email: true, length: {maximum: 200}, if: -> { !bot }
-
-
validates :name, presence: true, if: :require_valid_signup
-
validates :legal_accepted, presence: true, if: :require_legal_accepted
-
validate :validate_recaptcha, if: :require_recaptcha
-
-
has_one_attached :uploaded_avatar
-
-
validates_uniqueness_of :email, conditions: -> { where(email_verified: true) }, if: :email_verified?
-
validates_uniqueness_of :username, if: :email
-
before_validation :generate_username, if: :email
-
validates_length_of :name, maximum: 100
-
validates_length_of :username, maximum: 30
-
validates_length_of :short_bio, maximum: 5000
-
validates_format_of :username, with: /\A[a-z0-9]*\z/, message: I18n.t(:'user.error.username_must_be_alphanumeric')
-
validates_confirmation_of :password, if: :password_required?
-
-
validates_length_of :password, minimum: 8, allow_nil: true
-
-
has_many :admin_memberships,
-
-> { where('memberships.admin': true, revoked_at: nil) },
-
class_name: 'Membership'
-
-
has_many :memberships, -> { active }, dependent: :destroy
-
has_many :all_memberships, dependent: :destroy, class_name: "Membership"
-
-
has_many :adminable_groups,
-
-> { where(archived_at: nil) },
-
through: :admin_memberships,
-
class_name: 'Group',
-
source: :group
-
-
has_many :membership_requests,
-
foreign_key: 'requestor_id',
-
dependent: :destroy
-
-
has_many :groups,
-
-> { where archived_at: nil },
-
through: :memberships
-
-
has_many :discussions, through: :groups
-
-
has_many :authored_discussions, class_name: 'Discussion', foreign_key: 'author_id', dependent: :destroy
-
has_many :authored_polls, class_name: 'Poll', foreign_key: :author_id, dependent: :destroy
-
has_many :created_groups, class_name: 'Group', foreign_key: :creator_id, dependent: :destroy
-
-
has_many :identities, class_name: "Identities::Base", dependent: :destroy
-
-
has_many :reactions, dependent: :destroy
-
has_many :stances, foreign_key: :participant_id, dependent: :destroy
-
has_many :participated_polls, through: :stances, source: :poll
-
has_many :group_polls, through: :groups, source: :polls
-
-
has_many :discussion_readers, dependent: :destroy
-
has_many :guest_discussion_readers, -> { DiscussionReader.active.guests }, class_name: 'DiscussionReader', dependent: :destroy
-
has_many :guest_discussions, through: :guest_discussion_readers, source: :discussion
-
has_many :guest_stances, -> { Stance.latest.guests }, class_name: 'Stance', dependent: :destroy, foreign_key: :participant_id
-
has_many :guest_polls, through: :guest_stances, source: :poll
-
has_many :notifications, dependent: :destroy
-
has_many :comments, dependent: :destroy
-
has_many :documents, foreign_key: :author_id, dependent: :destroy
-
has_many :login_tokens, dependent: :destroy
-
has_many :events, dependent: :destroy
-
-
has_many :tags, through: :groups
-
-
before_save :set_avatar_initials
-
initialized_with_token :unsubscribe_token, -> { Devise.friendly_token }
-
initialized_with_token :email_api_key, -> { SecureRandom.hex(16) }
-
initialized_with_token :api_key, -> { SecureRandom.hex(16) }
-
-
enum default_membership_volume: [:mute, :quiet, :normal, :loud]
-
-
scope :active, -> { where(deactivated_at: nil) }
-
scope :deactivated, -> { where("deactivated_at IS NOT NULL") }
-
scope :sorted_by_name, -> { order("lower(name)") }
-
scope :admins, -> { where(is_admin: true) }
-
scope :coordinators, -> { joins(:memberships).where('memberships.admin = ?', true).group('users.id') }
-
scope :verified, -> { where(email_verified: true) }
-
scope :unverified, -> { where(email_verified: false) }
-
scope :search_for, -> (q) { where("users.name ilike :first OR users.name ilike :other OR users.username ilike :first OR users.email ilike :first", first: "#{q}%", other: "% #{q}%") }
-
scope :visible_by, -> (user) { distinct.active.verified.joins(:memberships).where("memberships.group_id": user.group_ids).where.not(id: user.id) }
-
scope :humans, -> { where(bot: false) }
-
scope :bots, -> { where(bot: true) }
-
-
scope :mention_search, -> (user, model, query) do
-
return self.none unless model.present?
-
ids = []
-
-
if model.group_id
-
ids += Membership.active.where(group_id: model.group_id).pluck(:user_id) if model.group_id
-
end
-
-
if model.discussion_id
-
ids += DiscussionReader.active.guests.where(discussion_id: model.discussion_id).pluck(:user_id)
-
end
-
-
if model.poll_id
-
ids += Stance.latest.guests.where(poll_id: model.poll_id).pluck(:participant_id)
-
end
-
-
if model.respond_to?(:poll_ids) and model.poll_ids.any?
-
ids += Stance.latest.guests.where(poll_id: model.poll_ids).pluck(:participant_id)
-
end
-
-
active.search_for(query).where(id: ids)
-
end
-
-
scope :email_when_proposal_closing_soon, -> { active.where(email_when_proposal_closing_soon: true) }
-
-
scope :email_proposal_closing_soon_for, -> (group) {
-
email_when_proposal_closing_soon
-
.joins(:memberships)
-
.where('memberships.group_id': group.id)
-
}
-
-
def default_format
-
if experiences['html-editor.uses-markdown']
-
'md'
-
else
-
'html'
-
end
-
end
-
-
def date_time_pref
-
self[:date_time_pref] || 'day_abbr'
-
end
-
-
def author
-
self
-
end
-
-
def is_paying?
-
group_ids = self.group_ids.concat(self.groups.pluck(:parent_id).compact).uniq
-
Group.where(id: group_ids).where(parent_id: nil).joins(:subscription).where.not('subscriptions.plan': 'trial').exists?
-
end
-
-
def is_paying
-
is_paying?
-
end
-
-
def invitations_rate_limit
-
if user.is_paying?
-
ENV.fetch('PAID_INVITATIONS_RATE_LIMIT', 50000)
-
else
-
ENV.fetch('TRIAL_INVITATIONS_RATE_LIMIT', 500)
-
end.to_i
-
end
-
-
def browseable_group_ids
-
Group.where(
-
"id in (:group_ids) OR
-
(parent_id in (:group_ids) AND is_visible_to_parent_members = TRUE)",
-
group_ids: self.group_ids).pluck(:id)
-
end
-
-
def set_legal_accepted_at
-
self.legal_accepted_at = Time.now
-
end
-
-
def require_legal_accepted
-
self.require_valid_signup && ENV['TERMS_URL']
-
end
-
-
def self.email_status_for(email)
-
find_by(email: email)&.email_status || :unused
-
end
-
-
def self.find_for_database_authentication(warden_conditions)
-
super(warden_conditions.merge(email_verified: true))
-
end
-
-
define_counter_cache(:memberships_count) {|user| user.memberships.count }
-
-
def associate_with_identity(identity)
-
if existing = identities.find_by(user: self, uid: identity.uid, identity_type: identity.identity_type)
-
existing.update(access_token: identity.access_token)
-
identity = existing
-
else
-
identities.push(identity)
-
end
-
-
update(name: identity.name) if self.name.nil?
-
identity.assign_logo! unless self.avatar_url
-
self
-
end
-
-
def identity_for(type)
-
identities.find_by(identity_type: type)
-
end
-
-
def first_name
-
name.split(' ').first
-
end
-
-
def last_name
-
name.split(' ').drop(1).join(' ')
-
end
-
-
def remember_me
-
true
-
end
-
-
def is_logged_in?
-
true
-
end
-
-
def has_password
-
self.encrypted_password.present?
-
end
-
-
def email_status
-
if deactivated_at.present? then :inactive else :active end
-
end
-
-
def name_and_email
-
"\"#{name}\" <#{email}>"
-
end
-
-
# Provide can? and cannot? as methods for checking permissions
-
def ability
-
@ability ||= ::Ability::Base.new(self)
-
end
-
-
delegate :can?, :cannot?, :to => :ability
-
-
def is_member_of?(group)
-
!!memberships.find_by(group_id: group&.id)
-
end
-
-
def is_admin_of?(group)
-
!!memberships.find_by(group_id: group&.id, admin: true)
-
end
-
-
def first_name
-
self.name.to_s.split(' ').first
-
end
-
-
def time_zone
-
return 'UTC' if self[:time_zone] == "Etc/Unknown"
-
self[:time_zone] || 'UTC'
-
end
-
-
def self.helper_bot
-
verified.find_by(email: BaseMailer::NOTIFICATIONS_EMAIL_ADDRESS) ||
-
create!(email: BaseMailer::NOTIFICATIONS_EMAIL_ADDRESS,
-
name: 'Loomio Helper Bot',
-
password: SecureRandom.hex(20),
-
email_verified: true,
-
bot: true,
-
avatar_kind: :gravatar)
-
end
-
-
def name
-
if deactivated_at && self[:name].nil?
-
I18n.t('profile_page.deleted_account')
-
else
-
self[:name]
-
end
-
end
-
-
def name_or_username
-
self[:name] || self[:username]
-
end
-
-
# http://stackoverflow.com/questions/5140643/how-to-soft-delete-user-with-devise/8107966#8107966
-
def active_for_authentication?
-
super && !deactivated_at
-
end
-
-
def locale
-
first_supported_locale([selected_locale, detected_locale].compact)
-
end
-
-
def update_detected_locale(locale)
-
self.update_attribute(:detected_locale, locale) if self.detected_locale&.to_sym != locale.to_sym
-
end
-
-
def generate_username
-
self.username ||= ::UsernameGenerator.new(self).generate
-
end
-
-
def send_devise_notification(notification, *args)
-
I18n.with_locale(locale) { devise_mailer.send(notification, self, *args).deliver_now }
-
end
-
-
def self.ransackable_attributes(auth_object = nil)
-
[
-
"avatar_initials",
-
"avatar_kind",
-
"city",
-
"content_locale",
-
"country",
-
"created_at",
-
"current_sign_in_at",
-
"current_sign_in_ip",
-
"date_time_pref",
-
"deactivated_at",
-
"detected_locale",
-
"email",
-
"email_catch_up",
-
"email_catch_up_day",
-
"email_newsletter",
-
"email_on_participation",
-
"email_verified",
-
"email_when_mentioned",
-
"email_when_proposal_closing_soon",
-
"id",
-
"is_admin",
-
"key",
-
"last_seen_at",
-
"last_sign_in_at",
-
"last_sign_in_ip",
-
"legal_accepted_at",
-
"link_previews",
-
"location",
-
"locked_at",
-
"memberships_count",
-
"name",
-
"region",
-
"secret_token",
-
"selected_locale",
-
"short_bio",
-
"short_bio_format",
-
"sign_in_count",
-
"time_zone",
-
"updated_at",
-
"uploaded_avatar_content_type",
-
"uploaded_avatar_file_name",
-
"uploaded_avatar_file_size",
-
"uploaded_avatar_updated_at",
-
"username"]
-
end
-
-
protected
-
-
def password_required?
-
!password.nil? || !password_confirmation.nil?
-
end
-
-
private
-
-
def validate_recaptcha
-
return unless ENV['RECAPTCHA_APP_KEY']
-
return if Clients::Recaptcha.instance.validate(self.recaptcha)
-
# Sentry.capture_message("recaptcha failed", extra: {email: email})
-
self.errors.add(:recaptcha, I18n.t(:"user.error.recaptcha"))
-
end
-
end
-
1
class AttachmentQuery
-
1
def self.find(group_ids, query, limit, offset)
-
2
ids = []
-
2
ids.concat ActiveStorage::Attachment.joins(:blob).
-
joins("LEFT OUTER JOIN groups ON active_storage_attachments.record_type = 'Group' AND active_storage_attachments.record_id = groups.id").
-
where('groups.id IN (:group_ids)', group_ids: group_ids).
-
where('active_storage_attachments.name': :files).
-
where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
-
-
2
ids.concat ActiveStorage::Attachment.joins(:blob).
-
joins("LEFT OUTER JOIN comments ON active_storage_attachments.record_type = 'Comment' AND active_storage_attachments.record_id = comments.id").
-
joins("LEFT OUTER JOIN discussions comments_discussions ON comments_discussions.id = comments.discussion_id").
-
where('comments_discussions.group_id IN (:group_ids) AND comments_discussions.discarded_at IS NULL AND comments.discarded_at IS NULL', group_ids: group_ids).
-
where('active_storage_attachments.name': :files).
-
where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
-
-
2
ids.concat ActiveStorage::Attachment.joins(:blob).
-
joins("LEFT OUTER JOIN outcomes ON active_storage_attachments.record_type = 'Outcome' AND active_storage_attachments.record_id = outcomes.id").
-
joins("LEFT OUTER JOIN polls outcomes_polls ON outcomes_polls.id = outcomes.poll_id").
-
where('outcomes_polls.group_id IN (:group_ids) AND outcomes_polls.discarded_at IS NULL', group_ids: group_ids).
-
where('active_storage_attachments.name': :files).
-
where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
-
-
2
ids.concat ActiveStorage::Attachment.joins(:blob).
-
joins("LEFT OUTER JOIN stances ON active_storage_attachments.record_type = 'Stance' AND active_storage_attachments.record_id = stances.id").
-
joins("LEFT OUTER JOIN polls stances_polls ON stances_polls.id = stances.id").
-
where('stances_polls.group_id IN (:group_ids) AND stances_polls.discarded_at IS NULL AND stances.revoked_at IS NULL', group_ids: group_ids).
-
where('active_storage_attachments.name': :files).
-
where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
-
-
2
ids.concat ActiveStorage::Attachment.joins(:blob).
-
joins("LEFT OUTER JOIN discussions ON active_storage_attachments.record_type = 'Discussion' AND discussions.id = active_storage_attachments.record_id").
-
where('discussions.group_id IN (:group_ids) AND discussions.discarded_at IS NULL', group_ids: group_ids).
-
where('active_storage_attachments.name': :files).
-
where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
-
-
2
ids.concat ActiveStorage::Attachment.joins(:blob).
-
joins("LEFT OUTER JOIN polls ON active_storage_attachments.record_type = 'Poll' AND polls.id = active_storage_attachments.record_id").
-
where('polls.group_id IN (:group_ids) AND polls.discarded_at IS NULL', group_ids: group_ids).
-
where('active_storage_attachments.name': :files).
-
where("active_storage_blobs.filename ilike ?", "%#{query}%").limit(limit).offset(offset).order('id desc').pluck(:id)
-
-
2
ActiveStorage::Attachment.joins(:blob).where(id: ids).order('id desc')
-
end
-
end
-
1
class ContactableQuery
-
1
def self.contactable(user:, actor:)
-
# the users have a group (membership or membership request) in common
-
# the users have a discussion or poll in common
-
# membership, membership_request, discussion or poll in common.
-
5
(group_ids(user) & group_ids(actor)).any? ||
-
3
(discussion_ids(user) & discussion_ids(actor)).any? ||
-
2
(poll_ids(user) & poll_ids(actor)).any?
-
end
-
-
1
private
-
1
def self.group_ids(user)
-
10
%w[all_memberships
-
membership_requests
-
discussions
-
group_polls
-
guest_discussions
-
participated_polls].map do |relation|
-
60
user.send(relation).pluck(:group_id)
-
end.flatten.uniq
-
end
-
-
1
def self.discussion_ids(user)
-
6
%w[discussions guest_discussions].map do |relation|
-
12
user.send(relation).pluck(:id)
-
end.flatten.uniq
-
end
-
-
1
def self.poll_ids(user)
-
4
%w[group_polls participated_polls].map do |relation|
-
8
user.send(relation).pluck(:id)
-
end.flatten.uniq
-
end
-
end
-
1
class DiscussionQuery
-
1
def self.start
-
158
Discussion.
-
kept.
-
joins('LEFT OUTER JOIN groups ON discussions.group_id = groups.id').
-
where('groups.archived_at IS NULL').
-
includes(:author, :group)
-
end
-
-
1
def self.dashboard(chain: start, user: )
-
4
chain = chain.where("discussions.group_id IN (:group_ids) OR discussions.id IN (:discussion_ids)",
-
group_ids: user.group_ids, discussion_ids: user.guest_discussion_ids)
-
end
-
-
1
def self.inbox(chain: start, user: )
-
4
chain.joins("LEFT OUTER JOIN discussion_readers dr ON discussions.id = dr.discussion_id AND dr.user_id = #{user.id}").
-
where("discussions.group_id IN (:group_ids) OR discussions.id IN (:discussion_ids)", group_ids: user.group_ids, discussion_ids: user.guest_discussion_ids).
-
where('dr.dismissed_at IS NULL OR (dr.dismissed_at < discussions.last_activity_at)').
-
where('dr.last_read_at IS NULL OR (dr.last_read_at < discussions.last_activity_at)')
-
end
-
-
1
def self.visible_to(chain: start,
-
user: LoggedOutUser.new,
-
group_ids: [],
-
discussion_ids: [],
-
tags: [],
-
or_public: true,
-
or_subgroups: true,
-
only_direct: false,
-
only_unread: false)
-
-
150
if user.discussion_reader_token
-
or_discussion_reader_token = "OR dr.token = #{ActiveRecord::Base.connection.quote(user.discussion_reader_token)}"
-
end
-
-
150
chain = chain.joins("LEFT OUTER JOIN discussion_readers dr
-
ON dr.discussion_id = discussions.id
-
AND (dr.user_id = #{user.id || 0} #{or_discussion_reader_token})")
-
.where("#{'(discussions.private = false) OR ' if or_public}
-
(discussions.group_id in (:user_group_ids)) OR
-
(dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.guest = TRUE)
-
#{'OR (groups.parent_members_can_see_discussions = TRUE AND groups.parent_id IN (:user_group_ids))' if or_subgroups}", user_group_ids: user.group_ids)
-
-
150
chain = chain.where("discussions.group_id IN (?)", group_ids) if Array(group_ids).any?
-
150
chain = chain.where("discussions.id IN (?)", discussion_ids) if Array(discussion_ids).any?
-
150
chain = chain.where("discussions.group_id IS NULL") if only_direct
-
150
chain = chain.where("tags @> ARRAY[?]::varchar[]", tags) if tags.any?
-
-
150
if only_unread
-
7
chain = chain.where('(dr.dismissed_at IS NULL) OR (dr.dismissed_at < discussions.last_activity_at)').
-
where('dr.last_read_at IS NULL OR (dr.last_read_at < discussions.last_activity_at)')
-
end
-
-
150
chain
-
end
-
-
1
def self.filter(chain: , filter: )
-
7
case filter
-
when 'show_closed', 'closed' then chain.is_closed
-
when 'all' then chain
-
7
else chain.is_open
-
end.order_by_latest_activity
-
end
-
end
-
1
class GroupQuery
-
1
def self.start
-
4
Group.includes(:subscription, :creator, :parent)
-
end
-
-
1
def self.visible_to(user: LoggedOutUser.new, chain: start, show_public: false)
-
4
guest_discussion_group_ids = Discussion.where(id: user.guest_discussion_ids).pluck(:group_id)
-
4
group_ids = user.group_ids.concat(guest_discussion_group_ids)
-
4
chain.published.
-
where("#{'is_visible_to_public = true OR ' if show_public}
-
groups.id in (:group_ids) OR
-
(parent_id in (:group_ids) AND is_visible_to_parent_members = TRUE)", group_ids: group_ids)
-
end
-
end
-
1
class MembershipQuery
-
1
def self.start
-
6
Membership.includes(:group, :user, :inviter).joins(:group).joins(:user).active
-
end
-
-
1
def self.visible_to(user: , chain: start)
-
6
chain.where("memberships.group_id IN (#{ids_or_null(user.group_ids)}) OR
-
groups.parent_id IN (#{ids_or_null(user.adminable_group_ids)})")
-
end
-
-
1
def self.search(chain: start, params:)
-
5
if group = Group.find_by(id: params[:group_id])
-
5
group_ids = case params[:subgroups]
-
when 'mine', 'all'
-
group.id_and_subgroup_ids
-
else
-
5
[group.id]
-
end
-
5
chain = chain.where(group_id: group_ids)
-
end
-
-
5
if params[:user_ids]
-
chain = chain.where('memberships.user_id': params[:user_ids])
-
end
-
-
5
case params[:filter]
-
when 'admin'
-
chain = chain.admin
-
when 'pending'
-
chain = chain.pending
-
when 'accepted'
-
chain = chain.accepted
-
end
-
-
5
query = params[:q].to_s
-
5
if query.length > 0
-
1
chain = chain.where("users.name ilike :first OR users.name ilike :last OR
-
users.email ilike :first OR
-
users.username ilike :first",
-
first: "#{query}%", last: "% #{query}%")
-
end
-
-
5
chain
-
end
-
-
1
def self.ids_or_null(ids)
-
12
if ids.length == 0
-
4
'null'
-
else
-
8
ids.join(',')
-
end
-
end
-
end
-
1
class PollQuery
-
1
def self.start
-
279
Poll.distinct.kept.includes(:poll_options, :group, :author)
-
end
-
-
1
def self.visible_to(user: LoggedOutUser.new,
-
chain: start,
-
group_ids: [],
-
show_public: false)
-
-
279
if user.discussion_reader_token
-
or_discussion_reader_token = "OR dr.token = #{ActiveRecord::Base.connection.quote(user.discussion_reader_token)}"
-
end
-
-
279
if user.stance_token
-
or_stance_token = "OR s.token = #{ActiveRecord::Base.connection.quote(user.discussion_reader_token)}"
-
end
-
-
279
chain = chain.where('polls.group_id IN (:group_ids)', group_ids: group_ids) if group_ids.any?
-
279
chain = chain.joins("LEFT OUTER JOIN discussions d on d.id = polls.discussion_id")
-
279
chain = chain.joins("LEFT OUTER JOIN memberships m ON m.group_id = polls.group_id AND m.user_id = #{user.id || 0}")
-
.joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = polls.discussion_id AND (dr.user_id = #{user.id || 0} #{or_discussion_reader_token})")
-
.joins("LEFT OUTER JOIN stances s ON s.poll_id = polls.id AND (s.participant_id = #{user.id || 0} #{or_stance_token})")
-
.where("#{'d.private = false OR ' if show_public}
-
polls.author_id = :user_id OR
-
(m.id IS NOT NULL AND m.revoked_at IS NULL) OR
-
(dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.guest = TRUE) OR
-
(s.id IS NOT NULL AND s.revoked_at IS NULL AND s.guest = TRUE)", user_id: user.id)
-
279
chain
-
end
-
-
-
1
def self.filter(chain: , params: )
-
# how to do this....
-
8
if group = Group.find_by(key: params[:group_key])
-
1
group_ids = (params[:subgroups] == "none") ? [group.id] : group.id_and_subgroup_ids
-
1
chain = chain.where(group_id: group_ids)
-
end
-
-
8
if discussion = Discussion.find_by(key: params[:discussion_key]) || Discussion.find_by(id: params[:discussion_id])
-
2
chain = chain.where(discussion_id: discussion.id)
-
end
-
-
-
8
if (tags = (params[:tags] || '').split('|')).any?
-
chain = chain.where.contains(tags: tags)
-
end
-
-
8
if params[:status] == 'vote'
-
voted_poll_ids = Stance.where(latest: true).where.not(cast_at: nil).pluck(:poll_id)
-
chain = chain.where.not(id: voted_poll_ids)
-
end
-
-
8
chain = chain.where(template: true) if params[:template]
-
8
chain = chain.where(author_id: params[:author_id]) if params[:author_id]
-
8
chain = chain.where(poll_type: params[:poll_type]) if params[:poll_type]
-
8
chain = chain.send(params[:status]) if %w(active closed recent template).include?(params[:status])
-
8
chain = chain.search_for(params[:query]) if params[:query]
-
8
chain
-
end
-
end
-
1
class ReactionQuery
-
1
def self.start
-
2
Reaction.includes(:user)
-
end
-
-
1
def self.authorize!(user: LoggedOutUser.new, chain: start, params: )
-
2
discussion_ids = []
-
2
poll_ids = []
-
-
2
discussion_ids.concat(Comment.where(id: params[:comment_ids]).pluck(:discussion_id)) if params[:comment_ids]
-
2
discussion_ids.concat(params[:discussion_ids]) if params[:discussion_ids]
-
-
2
poll_ids.concat(Stance.where(id: params[:stance_ids]).pluck(:poll_id)) if params[:stance_ids]
-
2
poll_ids.concat(Outcome.where(id: params[:outcome_ids]).pluck(:poll_id)) if params[:outcome_ids]
-
2
poll_ids.concat(params[:poll_ids]) if params[:poll_ids]
-
-
2
discussion_ids.uniq!
-
2
poll_ids.uniq!
-
-
2
if (PollQuery.visible_to(user: user, show_public: true).where(id: poll_ids).count != poll_ids.length) ||
-
2
(DiscussionQuery.visible_to(user: user).where(id: discussion_ids).count != discussion_ids.length)
-
1
raise CanCan::AccessDenied.new
-
end
-
end
-
-
1
def self.unsafe_where(params)
-
ids = {
-
1
discussion_ids: Array(params[:discussion_ids]),
-
outcome_ids: Array(params[:outcome_ids]),
-
comment_ids: Array(params[:comment_ids]),
-
poll_ids: Array(params[:poll_ids]),
-
stance_ids: Array(params[:stance_ids])
-
}
-
1
Reaction.where(
-
"(reactable_type = 'Discussion' AND reactable_id IN (:discussion_ids)) OR
-
(reactable_type = 'Comment' AND reactable_id IN (:comment_ids)) OR
-
(reactable_type = 'Outcome' AND reactable_id IN (:outcome_ids)) OR
-
(reactable_type = 'Stance' AND reactable_id IN (:stance_ids)) OR
-
(reactable_type = 'Poll' AND reactable_id IN (:poll_ids))", ids)
-
end
-
end
-
1
class UserQuery
-
1
def self.relations(model:, actor:)
-
1731
rels = []
-
1731
if model.is_a?(Group) and model.members.exists?(actor.id)
-
13
rels.push User.joins('LEFT OUTER JOIN memberships m ON m.user_id = users.id').
-
where('m.group_id IN (:group_ids) AND m.revoked_at IS NULL', {group_ids: model.group.id})
-
end
-
-
-
1731
if model.nil? or actor.can?(:add_guests, model)
-
1591
group_ids = if model && model.group.present? && (!model.is_a?(Group) || model.parent_id)
-
1559
actor.group_ids & model.group.parent_or_self.id_and_subgroup_ids
-
else
-
32
actor.group_ids
-
end
-
-
1591
rels.push User.joins('LEFT OUTER JOIN memberships m ON m.user_id = users.id').
-
where('m.group_id IN (:group_ids) AND m.revoked_at IS NULL', {group_ids: group_ids})
-
-
# people who have been invited by actor
-
1591
rels.push(
-
User.joins("LEFT OUTER JOIN discussion_readers dr on dr.user_id = users.id").
-
where("dr.inviter_id = ? AND revoked_at IS NULL AND guest = TRUE", actor.id)
-
)
-
-
1591
rels.push(
-
User.joins("LEFT OUTER JOIN stances on stances.participant_id = users.id").
-
where("stances.inviter_id = ? AND revoked_at IS NULL AND guest = TRUE", actor.id)
-
)
-
end
-
-
1731
if model.present? && (actor.can?(:add_members, model) || actor.can?(:add_voters, model))
-
1603
if model.group.present?
-
1581
rels.push User.joins('LEFT OUTER JOIN memberships m ON m.user_id = users.id').
-
where('m.group_id IN (:group_ids) AND m.revoked_at IS NULL', {group_ids: model.group.id})
-
end
-
-
1603
if model.discussion_id
-
1200
rels.push(
-
User.joins('LEFT OUTER JOIN discussion_readers dr ON dr.user_id = users.id').
-
where('dr.discussion_id': model.discussion_id).where('dr.revoked_at IS NULL and dr.guest = TRUE')
-
)
-
-
1200
rels.push(
-
User.joins('LEFT OUTER JOIN stances ON stances.participant_id = users.id').
-
where('stances.poll_id': model.discussion.poll_ids).where("stances.revoked_at IS NULL and stances.guest = TRUE")
-
)
-
end
-
-
1603
if model.poll_id
-
795
rels.push(
-
User.joins('LEFT OUTER JOIN stances ON stances.participant_id = users.id').
-
where('stances.poll_id': model.poll_id).where("stances.revoked_at IS NULL AND stances.guest = TRUE")
-
)
-
end
-
end
-
1731
rels
-
end
-
-
1
def self.invitable_user_ids(model: , actor:, user_ids: )
-
1704
relations(model: model, actor: actor).map do |rel|
-
9468
rel.where(id: user_ids).pluck(:id)
-
end.flatten.uniq.compact
-
end
-
-
1
def self.invitable_search(model:, actor:, q: nil, limit: 50)
-
27
ids = relations(model: model, actor: actor).map do |rel|
-
94
rel.active.search_for(q).limit(limit).pluck(:id)
-
end.flatten.uniq.compact
-
27
User.where(id: ids).order(:memberships_count).limit(50)
-
end
-
end
-
1
class ApplicationSerializer < ActiveModel::Serializer
-
1
embed :ids, include: true
-
-
1
def scope
-
206867
super || {}
-
end
-
-
1
def tags
-
8091
cache_fetch([:tags_by_type_and_id, object.class.to_s], object.id) { object.tags }
-
end
-
-
1
def poll
-
2334
cache_fetch(:polls_by_id, object.poll_id) { object.poll }
-
end
-
-
1
def group
-
4398
cache_fetch(:groups_by_id, object.group_id) { object.group }
-
end
-
-
1
def event
-
2852
cache_fetch(:events_by_id, object.event_id)
-
end
-
-
1
def discussion
-
7974
cache_fetch(:discussions_by_id, object.discussion_id) { object.discussion }
-
end
-
-
1
def author
-
5145
cache_fetch(:users_by_id, object.author_id) { object.author }
-
end
-
-
1
def actor
-
10889
cache_fetch(:users_by_id, object.actor_id) { object.actor }
-
end
-
-
1
def user
-
411
cache_fetch(:users_by_id, object.user_id) { object.user }
-
end
-
-
1
def inviter
-
262
cache_fetch(:users_by_id, object.inviter_id) { object.inviter }
-
end
-
-
1
def self.hide_when_discarded(names)
-
3
Array(names).each do |name|
-
36
define_method name do
-
13711
object.discarded_at ? nil : object.send(name)
-
end
-
end
-
end
-
-
1
def cache_fetch(key_or_keys, id)
-
90818
return nil if id.nil?
-
80929
if scope.has_key?(:cache)
-
102640
scope[:cache].fetch(key_or_keys, id) { yield }
-
else
-
222
yield
-
end
-
end
-
-
1
def include_type?(type)
-
23800
!Array(scope[:exclude_types]).include?(type)
-
end
-
-
1
def exclude_type?(type)
-
1708
Array(scope[:exclude_types]).include?(type)
-
end
-
-
1
def include_reactions?
-
include_type?('reaction')
-
end
-
-
1
def include_current_user_membership?
-
1019
include_type?('membership')
-
end
-
-
1
def include_discussion?
-
3292
include_type?('discussion')
-
end
-
-
1
def include_poll?
-
190
include_type?('poll')
-
end
-
-
1
def include_created_event?
-
1362
include_type?('event')
-
end
-
-
1
def include_forked_event?
-
990
include_type?('event')
-
end
-
-
1
def include_group?
-
1480
include_type?('group') && object.group_id
-
end
-
-
1
def include_active_polls?
-
990
include_type?('poll')
-
end
-
-
1
def include_eventable?
-
true
-
end
-
-
1
def include_poll_options?
-
372
include_type?('poll_option')
-
end
-
-
1
def include_stances?
-
include_type?('stance')
-
end
-
-
1
def include_my_stance?
-
include_type?('stance') && scope[:current_user_id].present?
-
end
-
-
1
def include_stance_choices?
-
include_type?('stance_choice')
-
end
-
-
1
def include_tags?
-
2411
include_type?('tag')
-
end
-
-
1
def include_participant?
-
127
include_author?
-
end
-
-
1
def include_user?
-
136
include_type?('user')
-
end
-
-
1
def include_author?
-
1819
include_type?('user') and include_type?('author')
-
end
-
-
1
def include_parent?
-
3697
include_type?('parent')
-
end
-
-
1
def include_actor?
-
3616
include_type?('user')
-
end
-
-
1
def include_inviter?
-
118
include_type?('user') && include_type?('inviter')
-
end
-
-
1
def include_outcome?
-
include_type?('outcome')
-
end
-
-
1
def include_current_outcome?
-
372
include_type?('outcome')
-
end
-
-
1
def include_outcomes?
-
include_type?('outcome')
-
end
-
end
-
1
class AttachmentSerializer < ActiveModel::Serializer
-
1
embed :ids, include: true
-
1
attributes :id, :filename, :content_type, :byte_size, :icon,
-
:preview_url, :download_url, :created_at, :record_type, :record_id
-
-
1
has_one :record, polymorphic: true
-
1
has_one :author, serializer: AuthorSerializer, root: :users
-
-
1
def author
-
3
object.record.author
-
end
-
-
1
def preview_url
-
1
Rails.application.routes.url_helpers.rails_representation_path(object.representation(HasRichText::PREVIEW_OPTIONS), only_path: true) if object.representable?
-
end
-
-
1
def download_url
-
1
Rails.application.routes.url_helpers.rails_blob_path(object, only_path: true)
-
end
-
-
1
def filename
-
1
object.blob.filename
-
end
-
-
1
def content_type
-
3
object.blob.content_type
-
end
-
-
1
def byte_size
-
1
object.blob.byte_size
-
end
-
-
1
def icon
-
3
AppConfig.doctypes.detect{ |type| /#{type['regex']}/.match(content_type || filename) }['icon']
-
end
-
-
end
-
1
class AuthorSerializer < ApplicationSerializer
-
1
attributes :id,
-
:name,
-
:email,
-
:username,
-
:avatar_initials,
-
:avatar_kind,
-
:thumb_url,
-
:time_zone,
-
:locale,
-
:created_at,
-
:titles,
-
:placeholder_name,
-
:email_verified,
-
:bot
-
-
1
def include_email?
-
2509
scope[:current_user_id] == object.id || scope[:include_email] || scope[:current_user_is_admin]
-
end
-
-
1
def titles
-
2522
object.experiences['titles'] || {}
-
end
-
-
1
def avatar_kind
-
2522
if !object.email_verified && !object.name
-
8
'mdi-email-outline'
-
else
-
2514
object.avatar_kind
-
end
-
end
-
-
1
def placeholder_name
-
8
I18n.t("user.placeholder_name", hostname: object.email.to_s.split('@').last, locale: object.locale)
-
end
-
-
1
def include_placeholder_name?
-
2522
object.name.nil?
-
end
-
-
1
private
-
-
1
def scope
-
7220
super || {}
-
end
-
end
-
class ChatbotSerializer < ApplicationSerializer
-
attributes :id, :kind, :webhook_kind, :group_id, :server, :channel, :event_kinds, :name, :notification_only
-
-
def include_server?
-
scope && scope[:current_user_is_admin]
-
end
-
-
def include_channel?
-
include_server?
-
end
-
end
-
1
class CommentSerializer < ApplicationSerializer
-
1
attributes :id,
-
:body,
-
:body_format,
-
:mentioned_usernames,
-
:discussion_id,
-
:created_at,
-
:updated_at,
-
:parent_id,
-
:parent_type,
-
:content_locale,
-
:versions_count,
-
:attachments,
-
:link_previews,
-
:author_id,
-
:discarded_at
-
-
1
has_one :author, serializer: AuthorSerializer, root: :users
-
1
has_one :discussion, serializer: DiscussionSerializer, root: :discussions
-
-
-
1
hide_when_discarded [:body]
-
-
1
def include_mentioned_usernames?
-
242
body_format == "md"
-
end
-
-
1
def include_secret_token?
-
object.user_id == scope[:current_user_id]
-
end
-
end
-
class ContactMessageSerializer < ActiveModel::Serializer
-
attributes :name, :email, :message
-
end
-
class ContactRequestSerializer < ActiveModel::Serializer
-
end
-
class ContactSerializer < ActiveModel::Serializer
-
embed :ids, include: true
-
attributes :id,
-
:name,
-
:email,
-
:source
-
-
has_one :user, serializer: UserSerializer, root: :users
-
end
-
1
class CurrentUserSerializer < UserSerializer
-
1
attributes :email, :email_when_proposal_closing_soon, :email_catch_up_day,
-
:email_when_mentioned, :email_on_participation, :selected_locale,
-
:locale, :default_membership_volume, :experiences,
-
:email_newsletter, :is_admin, :memberships_count, :secret_token
-
-
1
def include_email?
-
13
true
-
end
-
-
1
def include_email_hash?
-
true
-
end
-
-
1
def include_has_password?
-
13
true
-
end
-
-
1
private
-
-
1
def from_scope(field)
-
Array(Hash(scope)[field])
-
end
-
end
-
class DemoSerializer < ApplicationSerializer
-
attributes :id,
-
:name,
-
:description,
-
:group_id,
-
:priority,
-
:demo_handle
-
-
-
has_one :author, serializer: AuthorSerializer, root: :users
-
has_one :group
-
end
-
1
class DiscussionReaderSerializer < ApplicationSerializer
-
1
attributes :id,
-
:user_id,
-
:discussion_id,
-
:read_ranges,
-
:last_read_at,
-
:dismissed_at,
-
:volume,
-
:inviter_id,
-
:guest,
-
:admin,
-
:revoked_at
-
-
1
has_one :user, serializer: AuthorSerializer, root: :users
-
# has_one :discussion, serializer: DiscussionSerializer, root: :discussions
-
-
1
def last_read_at
-
8
object.discussion.anonymous_polls_count == 0 ? object.last_read_at : nil
-
end
-
-
1
def read_ranges
-
8
object.discussion.anonymous_polls_count == 0 ? object.read_ranges : []
-
end
-
-
1
def volume
-
8
object[:volume]
-
end
-
end
-
1
class DiscussionSerializer < ApplicationSerializer
-
1
def self.attributes_from_reader(*attrs)
-
1
attrs.each do |attr|
-
9
case attr
-
155
when :discussion_reader_id then define_method attr, -> { reader.id }
-
1240
else define_method attr, -> { reader.send(attr) }
-
end
-
8919
define_method :"include_#{attr}?", -> { reader.present? }
-
end
-
1
attributes *attrs
-
end
-
-
1
attributes :id,
-
:key,
-
:group_id,
-
:title,
-
:tags,
-
:content_locale,
-
:description,
-
:description_format,
-
:discussion_template_id,
-
:ranges,
-
:items_count,
-
:last_comment_at,
-
:last_activity_at,
-
:closed_at,
-
:closer_id,
-
:seen_by_count,
-
:members_count,
-
:created_at,
-
:updated_at,
-
:private,
-
:versions_count,
-
:pinned_at,
-
:attachments,
-
:link_previews,
-
:mentioned_usernames,
-
:newest_first,
-
:max_depth,
-
:discarded_at,
-
:secret_token
-
-
1
attributes_from_reader :discussion_reader_id,
-
:discussion_reader_volume,
-
:discussion_reader_user_id,
-
:last_read_at,
-
:dismissed_at,
-
:read_ranges,
-
:revoked_at,
-
:inviter_id,
-
:admin
-
-
1
has_one :author, serializer: AuthorSerializer, root: :users
-
1
has_one :group, serializer: GroupSerializer, root: :groups
-
1
has_many :active_polls, serializer: PollSerializer, root: :polls
-
1
has_one :created_event, serializer: EventSerializer, root: :events
-
1
has_one :forked_event, serializer: EventSerializer, root: :events
-
1
has_one :closer, serializer: AuthorSerializer, root: :users
-
-
1
hide_when_discarded [:description, :title]
-
-
1
def include_closer?
-
990
object.closer_id.present?
-
end
-
-
1
def include_mentioned_usernames?
-
990
description_format == "md"
-
end
-
-
1
def active_polls
-
5121
cache_fetch(:polls_by_discussion_id, object.id) { [] }
-
end
-
-
1
def reader
-
# we don't initialize readers if no current user id, because discussions can be group messages
-
10296
cache_fetch(:discussion_readers_by_discussion_id, object.id) do
-
8856
return nil unless scope[:current_user_id]
-
1656
m = cache_fetch(:memberships_by_group_id, object.group_id) { nil }
-
1332
DiscussionReader.find_or_initialize_by(user_id: scope[:current_user_id], discussion_id: object.id) do |dr|
-
1314
dr.volume = (m && m.volume) || 'normal'
-
end
-
end
-
end
-
-
1
def created_event
-
3136
cache_fetch([:events_by_kind_and_eventable_id, 'new_discussion'], object.id) { object.created_event }
-
end
-
-
1
def forked_event
-
3960
cache_fetch([:events_by_kind_and_eventable_id, 'discussion_forked'], object.id) { nil }
-
end
-
end
-
class DiscussionTemplateSerializer < ActiveModel::Serializer
-
embed :ids, include: true
-
-
has_one :group, serializer: GroupSerializer, root: :groups
-
has_many :poll_templates, serializer: PollTemplateSerializer, root: :poll_templates
-
-
attributes :id,
-
:group_id,
-
:position,
-
:author_id,
-
:process_name,
-
:process_subtitle,
-
:process_introduction,
-
:process_introduction_format,
-
:tags,
-
:title,
-
:title_placeholder,
-
:description,
-
:description_format,
-
:content_locale,
-
:created_at,
-
:updated_at,
-
:discarded_at,
-
:max_depth,
-
:newest_first,
-
:poll_template_keys_or_ids,
-
:public,
-
:recipient_audience
-
end
-
1
class DocumentSerializer < ApplicationSerializer
-
1
attributes :id, :title, :icon, :color, :url, :download_url,
-
:web_url, :thumb_url, :model_id, :model_type,
-
:created_at, :group_id
-
-
1
has_one :author, serializer: AuthorSerializer, root: :users
-
-
1
def group_id
-
18
object.group&.id
-
end
-
-
1
def is_an_image?
-
36
object.doctype == 'image'
-
end
-
1
alias :include_web_url? :is_an_image?
-
1
alias :include_thumb_url? :is_an_image?
-
end
-
1
class EventSerializer < ApplicationSerializer
-
1
attributes :id, :sequence_id, :position, :depth, :child_count, :descendant_count, :kind,
-
:discussion_id, :created_at, :eventable_id, :eventable_type, :custom_fields,
-
:pinned, :pinned_title, :parent_id, :actor_id, :position_key, :recipient_message
-
-
1
has_one :actor, serializer: AuthorSerializer, root: :users
-
1
has_one :eventable, polymorphic: true
-
1
has_one :discussion, serializer: DiscussionSerializer, root: :discussions
-
1
has_one :parent, serializer: EventSerializer, root: :parent_events
-
-
# for discussion moved event
-
1
has_one :source_group, serializer: GroupSerializer, root: :groups
-
-
1
def parent
-
6195
cache_fetch(:events_by_id, object.parent_id) { object.parent }
-
end
-
-
1
def include_eventable?
-
2678
!(object.kind == "new_discussion" && exclude_type?('discussion'))
-
end
-
-
1
def eventable
-
16020
case object.eventable_type
-
10692
when 'Discussion' then cache_fetch(:discussions_by_id, object.eventable_id) { object.eventable }
-
3540
when 'Poll' then cache_fetch(:polls_by_id, object.eventable_id) { object.eventable }
-
1464
when 'Comment' then cache_fetch(:comments_by_id, object.eventable_id) { object.eventable }
-
438
when 'Stance' then cache_fetch(:stances_by_id, object.eventable_id) { object.eventable }
-
186
when 'Outcome' then cache_fetch(:outcomes_by_id, object.eventable_id) { object.eventable }
-
30
when 'Reaction' then cache_fetch(:reactions_by_id, object.eventable_id) { object.eventable }
-
66
when 'Membership' then cache_fetch(:memberships_by_id, object.eventable_id) { object.eventable }
-
when 'Group' then cache_fetch(:groups_by_id, object.eventable_id) { object.eventable }
-
when 'MembershipRequest' then cache_fetch(:membership_requests_by_id, object.eventable_id) { object.eventable }
-
else
-
# raise "waht is it? #{object.eventable} #{object.kind}"
-
object.eventable
-
end
-
end
-
-
1
def position_key
-
2678
if object.kind == "new_discussion"
-
1708
"00000"
-
else
-
970
object.position_key
-
end
-
end
-
-
1
def source_group
-
24
Group.find_by(id: object.custom_fields['source_group_id'])
-
end
-
-
1
def include_source_group?
-
2678
object.kind == "discussion_moved" && object.custom_fields['source_group_id'].present?
-
end
-
-
1
def pinned_title
-
2678
object.custom_fields['pinned_title']
-
end
-
-
1
def include_custom_fields?
-
2678
["poll_edited", "discussion_edited", "discussion_moved"].include? object.kind
-
end
-
-
end
-
1
class GroupSerializer < ApplicationSerializer
-
1
attributes :id,
-
:key,
-
:handle,
-
:name,
-
:full_name,
-
:content_locale,
-
:description,
-
:description_format,
-
:logo_url,
-
:created_at,
-
:creator_id,
-
:members_can_add_members,
-
:members_can_add_guests,
-
:members_can_announce,
-
:members_can_create_subgroups,
-
:members_can_start_discussions,
-
:members_can_edit_discussions,
-
:members_can_edit_comments,
-
:members_can_delete_comments,
-
:members_can_raise_motions,
-
:admins_can_edit_user_content,
-
:token,
-
:polls_count,
-
:poll_templates_count,
-
:closed_polls_count,
-
:discussions_count,
-
:public_discussions_count,
-
:group_privacy,
-
:memberships_count,
-
:pending_memberships_count,
-
:accepted_memberships_count,
-
:membership_granted_upon,
-
:discussion_privacy_options,
-
:admin_memberships_count,
-
:archived_at,
-
:attachments,
-
:link_previews,
-
:new_threads_max_depth,
-
:new_threads_newest_first,
-
:cover_url,
-
:open_discussions_count,
-
:closed_discussions_count,
-
:discussion_templates_count,
-
:recent_activity_count,
-
:is_visible_to_public,
-
:is_visible_to_parent_members,
-
:parent_members_can_see_discussions,
-
:org_discussions_count,
-
:org_members_count,
-
:subscription,
-
:subgroups_count,
-
:new_host,
-
:secret_token,
-
:categorize_poll_templates
-
-
1
has_one :parent, serializer: GroupSerializer, root: :parent_groups
-
1
has_one :current_user_membership, serializer: MembershipSerializer, root: :memberships
-
1
has_many :tags, serializer: TagSerializer, root: :tags
-
-
1
def current_user_membership
-
6017
cache_fetch(:memberships_by_group_id, object.id) { nil }
-
end
-
-
1
def parent
-
2069
cache_fetch(:groups_by_id, object.parent_id) { object.parent }
-
end
-
-
1
def subscription
-
1216
sub = cache_fetch(:subscriptions_by_group_id, object.id) { object.subscription || Subscription.new }
-
{
-
1019
max_members: sub.max_members,
-
max_threads: sub.max_threads,
-
allow_subgroups: sub.allow_subgroups,
-
plan: sub.plan,
-
state: sub.state,
-
active: sub.is_active?,
-
renews_at: sub.renews_at,
-
expires_at: sub.expires_at,
-
members_count: sub.members_count
-
}
-
end
-
-
1
def include_secret_token?
-
1019
current_user_membership && current_user_membership.admin
-
end
-
-
1
def logo_url
-
1019
object.self_or_parent_logo_url
-
end
-
-
1
def cover_url
-
1019
object.self_or_parent_cover_url
-
end
-
-
1
def tag_names
-
object.info['tag_names'] || []
-
end
-
-
1
def new_host
-
1019
object.info['new_host'] || nil
-
end
-
-
1
private
-
1
def include_org_members_count?
-
1019
object.is_parent?
-
end
-
-
1
def include_org_discussions_count?
-
1019
object.is_parent?
-
end
-
-
1
def include_token?
-
1019
Hash(scope)[:include_token]
-
end
-
-
1
def has_discussions
-
object.discussions_count > 0
-
end
-
end
-
class IdentitySerializer < ActiveModel::Serializer
-
embed :ids, include: true
-
attributes :id, :identity_type, :user_id, :name, :email, :logo, :custom_fields
-
end
-
class LocaleSerializer < ActiveModel::Serializer
-
attributes :key, :name
-
-
def key
-
object
-
end
-
-
def name
-
I18n.with_locale(:en) { I18n.t(object, scope: :native_language_name) }
-
end
-
end
-
class MarkedAsRead::DiscussionSerializer < ActiveModel::Serializer
-
embed :ids, include: true
-
-
def self.attributes_from_reader(*attrs)
-
attrs.each do |attr|
-
case attr
-
when :discussion_reader_id then define_method attr, -> { reader.id }
-
else define_method attr, -> { reader.send(attr) }
-
end
-
define_method :"include_#{attr}?", -> { reader.present? }
-
end
-
attributes *attrs
-
end
-
-
attributes :id,
-
:key,
-
:items_count,
-
:ranges
-
-
attributes_from_reader :discussion_reader_id,
-
:read_ranges,
-
:last_read_at,
-
:dismissed_at
-
-
def reader
-
@reader ||= scope[:reader_cache].get_for(object) if scope[:reader_cache]
-
end
-
-
def scope
-
super || {}
-
end
-
end
-
1
class MemberEmailAliasSerializer < ApplicationSerializer
-
1
attributes :id,
-
:email,
-
:group_id,
-
:user_id,
-
:author_id,
-
:created_at
-
-
-
1
has_one :user, serializer: AuthorSerializer, root: :users, key: :user_id
-
1
has_one :author, serializer: AuthorSerializer, root: :users
-
-
1
def user_email
-
(cache_fetch(:users_by_id, object.user_id) { object.user }).email
-
end
-
-
1
def include_user_email?
-
true
-
end
-
end
-
class MemberSerializer < ActiveModel::Serializer
-
attributes :key, :priority, :type, :title, :subtitle, :logo_url, :logo_type, :last_notified_at
-
end
-
1
class MembershipRequestSerializer < ApplicationSerializer
-
1
attributes :id, :group_id, :name, :email, :introduction, :responded_at, :response, :created_at, :updated_at, :requestor_email
-
-
1
has_one :responder, serializer: AuthorSerializer, root: :users
-
1
has_one :requestor, serializer: AuthorSerializer, root: :users
-
-
1
def requestor_email
-
4
requestor&.email
-
end
-
end
-
1
class MembershipSerializer < ApplicationSerializer
-
1
attributes :id,
-
:group_id,
-
:user_id,
-
:inviter_id,
-
:volume,
-
:admin,
-
:experiences,
-
:title,
-
:created_at,
-
:accepted_at,
-
:user_email
-
-
1
has_one :group, serializer: GroupSerializer, root: :groups
-
1
has_one :user, serializer: UserSerializer, root: :users, key: :user_id
-
1
has_one :inviter, serializer: AuthorSerializer, root: :users
-
-
1
def user_email
-
20
(cache_fetch(:users_by_id, object.user_id) { object.user }).email
-
end
-
-
1
def include_user_email?
-
118
scope && (
-
118
object.inviter_id == scope[:current_user_id] ||
-
scope[:current_user_is_admin]
-
)
-
end
-
end
-
class Metadata::DiscussionSerializer < MetadataSerializer
-
attributes :title, :description, :image_urls
-
-
def description
-
render_plain_text(object.description, object.description_format)
-
end
-
-
def image_urls
-
[object.group.cover_url, object.group.logo_url]
-
end
-
end
-
class Metadata::GroupSerializer < MetadataSerializer
-
attributes :title, :description, :image_urls
-
-
def title
-
object.full_name
-
end
-
-
def description
-
if object.is_visible_to_public?
-
render_plain_text(object.description, object.description_format)
-
end
-
end
-
-
def image_urls
-
[object.group.cover_url, object.group.logo_url]
-
end
-
end
-
class Metadata::PollSerializer < MetadataSerializer
-
attributes :title, :description, :image_urls
-
-
def title
-
object.title
-
end
-
-
def description
-
render_plain_text(object.details, object.details_format)
-
end
-
-
def image_urls
-
object.group ? [object.group.cover_url, object.group.logo_url] : []
-
end
-
end
-
class Metadata::UserSerializer < MetadataSerializer
-
attributes :title, :description, :image_urls
-
-
def title
-
object.name
-
end
-
-
def description
-
render_plain_text(object.short_bio, object.short_bio_format)
-
end
-
-
def image_urls
-
[object.avatar_url]
-
end
-
end
-
class MetadataSerializer < ActiveModel::Serializer
-
include EmailHelper
-
root false
-
end
-
class ModelErrorSerializer < ActiveModel::Serializer
-
attributes :id, :messages
-
-
def messages
-
object.errors.full_messages
-
end
-
end
-
class Notification::EventSerializer < EventSerializer
-
def include_discussion?
-
false
-
end
-
-
def include_parent?
-
false
-
end
-
end
-
1
class NotificationSerializer < ApplicationSerializer
-
1
attributes :id,
-
:viewed,
-
:created_at,
-
:url,
-
:kind,
-
:actor_id,
-
:event_id,
-
:name,
-
:title,
-
:poll_type,
-
:reaction,
-
:model
-
-
1
has_one :actor, serializer: AuthorSerializer, root: :users
-
-
1
def name
-
938
tv :name
-
end
-
-
1
def title
-
938
tv :title
-
end
-
-
1
def poll_type
-
938
tv :poll_type
-
end
-
-
1
def reaction
-
938
tv :reaction
-
end
-
-
1
def model
-
938
tv :model
-
end
-
-
1
def tv(key)
-
4690
object.translation_values[key.to_s]
-
end
-
-
1
def kind
-
938
if event.kind == "announcement_created"
-
event.custom_fields['kind'] || "group_announced"
-
938
elsif event.kind == 'user_mentioned' &&
-
event.eventable.respond_to?(:parent) &&
-
event.eventable.parent.present? &&
-
event.eventable.parent.author == object.user
-
3
"comment_replied_to"
-
else
-
935
event.kind
-
end
-
end
-
end
-
1
class OutcomeSerializer < ApplicationSerializer
-
1
attributes :id,
-
:statement,
-
:statement_format,
-
:content_locale,
-
:latest,
-
:created_at,
-
:event_summary,
-
:event_location,
-
:attachments,
-
:link_previews,
-
:event_summary,
-
:review_on,
-
:event_location,
-
:poll_id,
-
:poll_option_id,
-
:group_id,
-
:author_id,
-
:secret_token,
-
:versions_count
-
-
1
has_one :author, serializer: AuthorSerializer, root: :users
-
1
has_one :poll, serializer: PollSerializer, root: :polls
-
-
1
def group_id
-
33
(cache_fetch(:polls_by_id, poll_id) { object.poll }).group_id
-
end
-
end
-
class Pending::BaseSerializer < ActiveModel::Serializer
-
embed :ids, include: true
-
attributes :name, :email, :email_status, :email_verified, :has_password, :identity_type,
-
:avatar_kind, :avatar_initials, :thumb_url, :avatar_url, :has_token, :auth_form
-
-
def identity_type
-
false
-
end
-
-
def auth_form
-
if user.email_status == :inactive && !has_token
-
:inactive
-
elsif (user.email_verified || has_token) && user.name
-
:signIn
-
else
-
:signUp
-
end
-
end
-
-
def has_token
-
# pending login or invitation token
-
end
-
-
def avatar_url
-
user.avatar_url
-
end
-
-
def thumb_url
-
user.thumb_url
-
end
-
-
def avatar_kind
-
user.avatar_kind
-
end
-
-
def avatar_initials
-
user.avatar_initials
-
end
-
-
def email_status
-
user.email_status
-
end
-
-
def email_verified
-
user.email_verified
-
end
-
-
def has_password
-
user.has_password
-
end
-
-
private
-
-
def user
-
@user ||= User.verified.find_by(email: email) || LoggedOutUser.new
-
end
-
end
-
class Pending::DiscussionReaderSerializer < Pending::MembershipSerializer
-
def identity_type
-
:discussion_reader
-
end
-
-
def group_id
-
nil
-
end
-
end
-
class Pending::GroupSerializer < Pending::BaseSerializer
-
attributes :token, :group_id
-
-
def auth_form
-
false
-
end
-
-
def has_token
-
true
-
end
-
-
def token
-
object.token
-
end
-
-
def include_email_status?
-
false
-
end
-
-
def include_email?
-
false
-
end
-
-
def email
-
nil
-
end
-
-
def name
-
nil
-
end
-
-
def identity_type
-
:group
-
end
-
-
def group_id
-
object.id
-
end
-
end
-
class Pending::IdentitySerializer < Pending::BaseSerializer
-
def auth_form
-
:identity
-
end
-
-
def identity_type
-
object.identity_type
-
end
-
-
def avatar_kind
-
if object.logo.present?
-
'uploaded'
-
else
-
'initials'
-
end
-
end
-
-
def avatar_url
-
object.logo
-
end
-
end
-
class Pending::MembershipSerializer < Pending::BaseSerializer
-
attributes :token, :group_id
-
-
def auth_form
-
false
-
end
-
-
def identity_type
-
:membership
-
end
-
-
def has_token
-
true
-
end
-
-
def email_status
-
nil
-
end
-
-
def avatar_initials
-
object.user&.get_avatar_initials
-
end
-
-
def name
-
object.user&.name
-
end
-
-
def email
-
object.user&.email
-
end
-
-
def group_id
-
object.group_id
-
end
-
-
private
-
-
def has_name?
-
object.user&.name.present?
-
end
-
-
alias :include_avatar_initials? :has_name?
-
end
-
class Pending::StanceSerializer < Pending::MembershipSerializer
-
def identity_type
-
:stance
-
end
-
-
def group_id
-
nil
-
end
-
end
-
class Pending::TokenSerializer < Pending::BaseSerializer
-
attributes :legal_accepted_at
-
-
def has_token
-
true
-
end
-
-
def identity_type
-
'loomio'
-
end
-
-
def name
-
if object.is_reactivation
-
user[:name]
-
else
-
user.name
-
end
-
end
-
-
def email
-
user.email
-
end
-
-
def legal_accepted_at
-
user.legal_accepted_at
-
end
-
-
private
-
-
def user
-
@user ||= object.user
-
end
-
-
def email_status
-
return :active if object.is_reactivation
-
User.email_status_for(email)
-
end
-
end
-
class Pending::UserSerializer < Pending::BaseSerializer
-
attributes :legal_accepted_at
-
-
private
-
-
def has_token
-
Hash(scope)[:has_token]
-
end
-
-
def user
-
object
-
end
-
-
def email_status
-
User.email_status_for(object.email)
-
end
-
end
-
class PermittedParamsSerializer < ActiveModel::Serializer
-
root false
-
-
def object
-
PermittedParams.new
-
end
-
-
PermittedParams::MODELS.each do |kind|
-
send :attribute, :"#{kind}_attributes", key: kind
-
end
-
-
end
-
class PluginSerializer < ActiveModel::Serializer
-
attributes :name, :config
-
end
-
1
class PollOptionSerializer < ApplicationSerializer
-
1
attributes :id, :poll_id, :name, :priority, :color, :icon, :meaning, :prompt
-
end
-
1
class PollSerializer < ApplicationSerializer
-
1
attributes :id,
-
:limit_reason_length,
-
:attachments,
-
:agree_target,
-
:author_id,
-
:anonymous,
-
:can_respond_maybe,
-
:chart_type,
-
:chart_column,
-
:closed_at,
-
:closing_at,
-
:created_at,
-
:content_locale,
-
:cast_stances_pct,
-
:decided_voters_count,
-
:details,
-
:details_format,
-
:discarded_at,
-
:discarded_by,
-
:discussion_id,
-
:group_id,
-
:hide_results,
-
:key,
-
:link_previews,
-
:mentioned_usernames,
-
:notify_on_closing_soon,
-
:poll_type,
-
:poll_option_names,
-
:poll_option_name_format,
-
:results,
-
:result_columns,
-
:reason_prompt,
-
:shuffle_options,
-
:stance_counts,
-
:specified_voters_only,
-
:secret_token,
-
:total_score,
-
:title,
-
:tags,
-
:undecided_voters_count,
-
:voter_can_add_options,
-
:voters_count,
-
:stance_reason_required,
-
:versions_count,
-
:dots_per_person,
-
:max_score,
-
:min_score,
-
:minimum_stance_choices,
-
:maximum_stance_choices,
-
:meeting_duration,
-
:poll_template_id,
-
:poll_template_key
-
-
1
has_one :discussion, serializer: DiscussionSerializer, root: :discussions
-
1
has_one :created_event, serializer: EventSerializer, root: :events
-
1
has_one :group, serializer: GroupSerializer, root: :groups
-
1
has_one :author, serializer: AuthorSerializer, root: :users
-
1
has_one :current_outcome, serializer: OutcomeSerializer, root: :outcomes
-
1
has_one :my_stance, serializer: StanceSerializer, root: :stances
-
1
has_many :poll_options, serializer: PollOptionSerializer, root: :poll_options
-
-
1
hide_when_discarded [
-
:attachments,
-
:link_previews,
-
:author_id,
-
:anonymous,
-
:can_respond_maybe,
-
:closed_at,
-
:closing_at,
-
:created_at,
-
:content_locale,
-
:cast_stances_pct,
-
:decided_voters_count,
-
:details,
-
:details_format,
-
:hide_results,
-
:limit_reason_length,
-
:notify_on_closing_soon,
-
:poll_type,
-
:poll_option_names,
-
:mentioned_usernames,
-
:results,
-
:shuffle_options,
-
:stance_counts,
-
:total_score,
-
:specified_voters_only,
-
:secret_token,
-
:title,
-
:undecided_voters_count,
-
:voter_can_add_options,
-
:voters_count,
-
:stance_reason_required,
-
:versions_count,
-
:meeting_duration,
-
:default_duration_in_days
-
]
-
-
1
def include_stance_counts?
-
372
poll.show_results?(voted: true)
-
end
-
-
1
def results
-
331
PollService.calculate_results(object, poll_options)
-
end
-
-
1
def include_results?
-
372
poll.show_results?(voted: true)
-
end
-
-
1
def current_outcome
-
1420
cache_fetch(:outcomes_by_poll_id, object.id) { nil }
-
end
-
-
1
def poll_options
-
1475
cache_fetch(:poll_options_by_poll_id, object.id) { object.poll_options }
-
end
-
-
1
def poll_option_names
-
377
cache_fetch(:poll_options_by_poll_id, object.id) { poll_options }.map(&:name)
-
end
-
-
1
def created_event
-
1094
cache_fetch([:events_by_kind_and_eventable_id, 'poll_created'], object.id) { object.created_event }
-
end
-
-
1
def include_mentioned_usernames?
-
372
details_format == "md"
-
end
-
-
1
def removed_poll_option_ids
-
object.poll_option_attributes.select { |attr| attr[:_destroy] }.map { |attr| attr[:id] }
-
end
-
-
1
def my_stance
-
840
cache_fetch(:my_stances_by_poll_id, object.id) { Stance.latest.find_by(poll_id: object.id, participant_id: scope[:current_user_id]) }
-
end
-
-
1
def include_my_stance?
-
372
my_stance.present?
-
end
-
end
-
class PollTemplateSerializer < ActiveModel::Serializer
-
embed :ids, include: true
-
-
has_one :group, serializer: GroupSerializer, root: :groups
-
-
attributes :id,
-
:key,
-
:group_id,
-
:position,
-
:author_id,
-
:poll_type,
-
:process_name,
-
:process_subtitle,
-
:process_introduction,
-
:process_introduction_format,
-
:tags,
-
:title,
-
:title_placeholder,
-
:details,
-
:details_format,
-
:anonymous,
-
:specified_voters_only,
-
:notify_on_closing_soon,
-
:content_locale,
-
:shuffle_options,
-
:hide_results,
-
:chart_type,
-
:min_score,
-
:max_score,
-
:minimum_stance_choices,
-
:maximum_stance_choices,
-
:dots_per_person,
-
:reason_prompt,
-
:poll_options,
-
:poll_option_name_format,
-
:stance_reason_required,
-
:limit_reason_length,
-
:default_duration_in_days,
-
:agree_target,
-
:created_at,
-
:updated_at,
-
:discarded_at,
-
:outcome_statement,
-
:outcome_statement_format,
-
:outcome_review_due_in_days
-
end
-
1
class ReactionSerializer < ApplicationSerializer
-
1
attributes :id, :reaction, :reactable_id, :reactable_type, :user_id
-
1
has_one :user, serializer: AuthorSerializer, root: :users
-
end
-
1
class ReceivedEmailSerializer < ApplicationSerializer
-
1
attributes :id, :group_id, :released, :subject, :sender_email, :sender_name, :dkim_valid, :spf_valid
-
end
-
class Restricted::GroupSerializer < ActiveModel::Serializer
-
embed :ids, include: true
-
attributes :id, :name, :logo_url, :cover_url
-
end
-
class Restricted::MembershipSerializer < ApplicationSerializer
-
embed :ids, include: true
-
attributes :id, :volume, :user_id, :group_id
-
has_one :group, serializer: Restricted::GroupSerializer, root: :groups
-
end
-
class Restricted::UserSerializer < ActiveModel::Serializer
-
embed :ids, include: true
-
attributes :id, :restricted, :username, :email, :email_when_proposal_closing_soon, :email_catch_up_day, :email_newsletter,
-
:email_when_mentioned, :email_on_participation, :default_membership_volume, :unsubscribe_token, :locale, :deactivated_at
-
has_many :memberships, serializer: Restricted::MembershipSerializer, root: :memberships
-
-
def restricted
-
true
-
end
-
end
-
1
class SearchResultSerializer < ApplicationSerializer
-
1
attributes :id,
-
:searchable_type,
-
:searchable_id,
-
:poll_title,
-
:discussion_title,
-
:discussion_key,
-
:highlight,
-
:poll_key,
-
:poll_id,
-
:sequence_id,
-
:group_id,
-
:group_handle,
-
:group_key,
-
:group_name,
-
:author_name,
-
:author_id,
-
:authored_at,
-
:tags
-
-
1
has_one :author, serializer: AuthorSerializer, root: :users
-
1
has_one :poll, serializer: PollSerializer, root: :polls
-
end
-
class SearchResults::BaseSerializer < ActiveModel::Serializer
-
attributes :id, :blurb
-
end
-
class SearchResults::CommentSerializer < SearchResults::BaseSerializer
-
has_one :author, serializer: UserSerializer
-
-
def author
-
User.find_by(id: object.user_id)
-
end
-
end
-
class SearchResults::DiscussionSerializer < ApplicationSerializer
-
attributes :title, :created_at, :group_name, :group_full_name, :key, :last_activity_at
-
end
-
class StanceChoiceSerializer < ApplicationSerializer
-
attributes :id, :score, :created_at, :stance_id, :rank, :rank_or_score, :poll_option_id
-
has_one :poll_option
-
-
def poll_option
-
cache_fetch(:poll_options_by_id, object.poll_option_id) { object.poll_option }
-
end
-
-
def stance
-
cache_fetch(:stances_by_id, object.stance_id) { object.stance }
-
end
-
-
def poll
-
cache_fetch(:polls_by_id, cache_fetch(:stances_by_id, object.stance_id).poll_id) { object.poll }
-
end
-
-
def rank
-
poll.minimum_stance_choices - object.score + 1 if poll.poll_type == 'ranked_choice'
-
end
-
-
def rank_or_score
-
rank || object.score
-
end
-
end
-
1
class StanceSerializer < ApplicationSerializer
-
1
attributes :id,
-
:reason,
-
:reason_format,
-
:content_locale,
-
:latest,
-
:admin,
-
:cast_at,
-
:mentioned_usernames,
-
:created_at,
-
:updated_at,
-
:locale,
-
:versions_count,
-
:attachments,
-
:link_previews,
-
:volume,
-
:inviter_id,
-
:poll_id,
-
:participant_id,
-
:revoked_at,
-
:order_at,
-
:option_scores
-
-
1
has_one :poll, serializer: PollSerializer, root: :polls
-
1
has_one :participant, serializer: AuthorSerializer, root: :users
-
-
1
def order_at
-
127
object.cast_at || object.created_at
-
end
-
-
1
def option_scores
-
111
if ENV['JIT_POLL_COUNTS'] && object.option_scores == {} && object.cast_at
-
object.update_option_scores!
-
end
-
111
object.option_scores
-
end
-
-
1
def include_option_scores?
-
127
include_reason?
-
end
-
-
1
def locale
-
127
participant&.locale || group&.locale
-
end
-
-
1
def participant
-
486
return nil if poll.anonymous?
-
420
cache_fetch(:users_by_id, object.participant_id) { object.participant }
-
end
-
-
1
def participant_id
-
127
return nil if poll.anonymous?
-
105
object.participant_id
-
end
-
-
1
def volume
-
127
object[:volume]
-
end
-
-
1
def include_reason?
-
635
!object.revoked_at && (object.participant_id == scope[:current_user_id] || poll.show_results?(voted: true))
-
end
-
-
1
def include_mentioned_usernames?
-
127
include_reason? && reason_format == 'md'
-
end
-
-
1
def include_attachments?
-
127
include_reason?
-
end
-
-
1
def include_link_previews?
-
127
include_reason?
-
end
-
end
-
1
class TagSerializer < ApplicationSerializer
-
1
attributes :id, :name, :color, :taggings_count, :org_taggings_count, :group_id, :priority
-
end
-
1
class TaskSerializer < ApplicationSerializer
-
1
attributes :id,
-
:name,
-
:author_id,
-
:uid,
-
:done,
-
:done_at,
-
:due_on,
-
:record_type,
-
:record_id
-
-
1
has_one :record, polymorphic: true, key: 'record_obj'
-
1
has_one :author, serializer: AuthorSerializer, root: :users
-
end
-
class TranslationSerializer < ActiveModel::Serializer
-
embed :ids, include: true
-
attributes :translatable_id, :translatable_type, :fields, :language
-
end
-
1
class UserSerializer < AuthorSerializer
-
1
attributes :short_bio,
-
:short_bio_format,
-
:content_locale,
-
:location,
-
:has_password,
-
:autodetect_time_zone,
-
:avatar_url,
-
:attachments,
-
:date_time_pref
-
-
1
def include_has_password?
-
89
scope[:include_password_status]
-
end
-
end
-
1
class VersionSerializer < ApplicationSerializer
-
1
attributes :id,
-
:whodunnit,
-
:previous_id,
-
:created_at,
-
:item_id,
-
:item_type,
-
:object_changes
-
-
# has_one :discussion
-
# has_one :comment
-
# has_one :poll
-
# has_one :stance
-
-
1
def whodunnit
-
1
object.whodunnit.to_i
-
end
-
-
1
def discussion
-
object.item
-
end
-
-
1
def poll
-
object.item
-
end
-
-
1
def comment
-
object.item
-
end
-
-
1
def stance
-
object.item
-
end
-
-
1
def previous_id
-
1
object.previous.try :id
-
end
-
-
1
def include_discussion?
-
object.item_type == 'Discussion'
-
end
-
-
1
def include_poll?
-
object.item_type == 'Poll'
-
end
-
-
1
def include_comment?
-
object.item_type == 'Comment'
-
end
-
-
1
def include_stance?
-
object.item_type == 'Stance'
-
end
-
end
-
class Webhook::Discord::EventSerializer < Webhook::Markdown::EventSerializer
-
attributes :content
-
-
def content
-
text
-
end
-
end
-
1
class Webhook::Markdown::EventSerializer < ActiveModel::Serializer
-
1
include PrettyUrlHelper
-
-
1
attributes :text,
-
:icon_url,
-
:username
-
-
1
def icon_url
-
38
(root_url(host: ENV['CANONICAL_HOST']).chomp('/') + (object.group.self_or_parent_logo_url(128) || ''))
-
end
-
-
1
def attachments
-
object.eventable.attachments
-
end
-
-
1
def username
-
38
AppConfig.theme[:site_name]
-
end
-
-
1
def text
-
38
I18n.with_locale(object.eventable.group.locale) do
-
38
ApplicationController.renderer.render(
-
layout: nil,
-
template: "chatbot/markdown/#{scope[:template_name]}",
-
assigns: { poll: object.eventable.poll, event: object, recipient: scope[:recipient] } )
-
end
-
end
-
-
1
private
-
-
1
def user
-
object.user || object.eventable.author
-
end
-
-
1
def eventable
-
object.eventable
-
end
-
end
-
class Webhook::Microsoft::EventSerializer < Webhook::Markdown::EventSerializer
-
attribute :type, key: :"@type"
-
attribute :context, key: :"@context"
-
attribute :theme_color, key: :themeColor
-
attributes :text, :sections
-
-
def type
-
"MessageCard"
-
end
-
-
def context
-
"http://schema.org/extensions"
-
end
-
-
def theme_color
-
AppConfig.theme[:primary_color]
-
end
-
-
def sections
-
[]
-
# [{
-
# activityTitle: "[#{section_title}](#{section_url})",
-
# activitySubtitle: section_subtitle,
-
# activityImage: section_image,
-
# facts: section_facts,
-
# markdown: true
-
# }]
-
end
-
-
def section_title
-
text_options[:title]
-
end
-
-
def section_url
-
text_options[:url]
-
end
-
-
def section_subtitle
-
object.eventable.description
-
end
-
-
def section_image
-
user.avatar_url
-
end
-
-
def section_facts
-
[]
-
end
-
end
-
class Webhook::Slack::EventSerializer < Webhook::Markdown::EventSerializer
-
def include_icon_url?
-
false
-
end
-
-
def include_username?
-
false
-
end
-
-
def text
-
I18n.with_locale(object.eventable.group.locale) do
-
ApplicationController.renderer.render(
-
layout: nil,
-
template: "chatbot/slack/#{scope[:template_name]}",
-
assigns: { poll: object.eventable.poll, event: object, recipient: scope[:recipient] } )
-
end
-
end
-
end
-
1
class AnnouncementService
-
1
class UnknownAudienceKindError < Exception; end
-
-
1
def self.audience_users(model, kind, actor, exclude_members = false, include_actor = false)
-
182
users = case kind
-
when /group-\d+/
-
id = kind.match(/group-(\d+)/)[1].to_i
-
group = model.group.parent_or_self.self_and_subgroups.find(id)
-
raise CanCan::AccessDenied unless actor.can?(:notify, group)
-
group.members
-
181
when 'group' then model.group.members
-
when 'discussion_group' then (model.discussion || NullDiscussion.new).readers
-
1
when 'voters' then (model.poll || NullPoll.new).unmasked_voters
-
when 'decided_voters' then (model.poll || NullPoll.new).unmasked_decided_voters
-
when 'undecided_voters' then (model.poll || NullPoll.new).unmasked_undecided_voters
-
when 'non_voters' then (model.poll || NullPoll.new).non_voters
-
when nil then User.none
-
else
-
raise UnknownAudienceKindError.new
-
end.active
-
-
182
users = users.where.not(id: (model.poll || NullPoll.new).voter_ids) if exclude_members
-
-
182
include_actor ? users.active.humans : users.active.humans.where('users.id != ?', actor.id)
-
end
-
-
1
def self.resend_pending_invitations(since: 25.hours.ago, till: 24.hours.ago)
-
Event.invitations_in_period(since, till).each { |event| Events::AnnouncementResend.publish!(event) }
-
end
-
end
-
1
class ChatbotService
-
1
def self.create(chatbot:, actor:)
-
actor.ability.authorize! :create, chatbot
-
return false unless chatbot.valid?
-
chatbot.author = actor
-
chatbot.save!
-
end
-
-
1
def self.update(chatbot:, params:, actor:)
-
actor.ability.authorize! :update, chatbot
-
params.delete(:access_token) unless params[:access_token].present?
-
chatbot.assign_attributes(params)
-
return false unless chatbot.valid?
-
chatbot.save!
-
end
-
-
1
def self.destroy(chatbot:, actor:)
-
actor.ability.authorize! :destroy, chatbot
-
chatbot.destroy
-
end
-
-
1
def self.publish_event!(event_id)
-
901
event = Event.find(event_id)
-
901
event.reload
-
901
return if event.eventable.nil?
-
-
901
chatbots = event.eventable.group.chatbots
-
-
901
CACHE_REDIS_POOL.with do |client|
-
901
chatbots.where(id: event.recipient_chatbot_ids).
-
or(chatbots.where.any(event_kinds: event.kind)).each do |chatbot|
-
# later, make a list and rpush into it. i guess
-
38
template_name = event.eventable_type.tableize.singularize
-
38
template_name = 'poll' if event.eventable_type == 'Outcome'
-
38
template_name = 'group' if event.eventable_type == 'Membership'
-
38
template_name = 'notification' if chatbot.notification_only
-
-
38
if %w[Poll Stance Outcome].include? event.eventable_type
-
16
poll = event.eventable.poll
-
end
-
-
38
example_user = chatbot.author || chatbot.group.creator
-
-
38
recipient = LoggedOutUser.new(locale: example_user.locale,
-
time_zone: example_user.time_zone,
-
date_time_pref: example_user.date_time_pref)
-
-
38
I18n.with_locale(recipient.locale) do
-
38
if chatbot.kind == "webhook"
-
38
serializer = "Webhook::#{chatbot.webhook_kind.classify}::EventSerializer".constantize
-
38
payload = serializer.new(event, root: false, scope: {template_name: template_name, recipient: recipient}).as_json
-
38
req = Clients::Webhook.new.post(chatbot.server, params: payload)
-
38
if req.response.code != 200
-
Sentry.capture_message("chatbot id #{chatbot.id} post event id #{event.id} failed: code: #{req.response.code} body: #{req.response.body}")
-
end
-
else
-
client.publish("chatbot/publish", {
-
config: chatbot.config,
-
payload: {
-
html: ApplicationController.renderer.render(
-
layout: nil,
-
template: "chatbot/matrix/#{template_name}",
-
assigns: { poll: poll, event: event, recipient: recipient } )
-
}
-
}.to_json)
-
end
-
end
-
end
-
end
-
end
-
-
1
def self.publish_test!(params)
-
case params[:kind]
-
when 'slack_webhook'
-
Clients::Webhook.new.post(params[:server], params: {text: I18n.t('chatbot.connection_test_successful')})
-
else
-
MAIN_REDIS_POOL.with do |client|
-
data = params.slice(:server, :access_token, :channel)
-
data.merge!(message: I18n.t('chatbot.connection_test_successful', group: params[:group_name]))
-
client.publish("chatbot/test", data.to_json)
-
end
-
end
-
end
-
end
-
module CleanupService
-
def self.delete_orphan_records
-
[Group,
-
Membership,
-
MembershipRequest,
-
Discussion,
-
Subscription,
-
DiscussionReader,
-
Comment,
-
Poll,
-
PollOption,
-
Stance,
-
StanceChoice,
-
Outcome,
-
Event,
-
Notification].each do |model|
-
count = model.dangling.delete_all
-
puts "deleted #{count} dangling #{model.to_s} records"
-
end
-
-
PaperTrail::Version.where(item_type: 'Motion').delete_all
-
ActiveStorage::Blob.unattached.where("active_storage_blobs.created_at < ?", 7.days.ago).find_each(&:purge_later)
-
-
# ["Comment", "Discussion", "Group", "Membership", "Outcome", "Poll", "Stance", "User"].each do |model|
-
# table = model.pluralize.downcase
-
# # puts PaperTrail::Version.joins("left join #{table} on #{table}.id = item_id and item_type = '#{model}'").where("#{table}.id is null").to_sql
-
# # puts PaperTrail::Version.joins("left join #{table} on #{table}.id = item_id and item_type = '#{model}'").where("#{table}.id is null").count
-
# count = PaperTrail::Version.joins("left join #{table} on #{table}.id = item_id and item_type = '#{model}'").where("#{table}.id is null").delete_all
-
# puts "deleted #{count} dangling #{table} version records"
-
# end
-
-
# real delete of dangling active storage objects
-
# delete subscription records where no group references them
-
end
-
end
-
1
class CommentService
-
-
1
def self.create(comment:, actor:)
-
188
actor.ability.authorize! :create, comment
-
187
comment.author = actor
-
187
return false unless comment.valid?
-
177
comment.save!
-
177
EventBus.broadcast('comment_create', comment, actor)
-
177
Events::NewComment.publish!(comment)
-
end
-
-
1
def self.discard(comment:, actor:)
-
3
actor.ability.authorize!(:discard, comment)
-
2
ActiveRecord::Base.transaction do
-
2
comment.update(discarded_at: Time.now, discarded_by: actor.id)
-
2
comment.created_event.update(user_id: nil, pinned: false)
-
end
-
2
comment.created_event
-
end
-
-
1
def self.undiscard(comment:, actor:)
-
actor.ability.authorize!(:undiscard, comment)
-
ActiveRecord::Base.transaction do
-
comment.update(discarded_at: nil, discarded_by: nil)
-
comment.created_event.update(user_id: comment.user_id)
-
end
-
comment.created_event
-
end
-
-
1
def self.destroy(comment:, actor:)
-
5
actor.ability.authorize!(:destroy, comment)
-
4
comment_id = comment.id
-
4
discussion_id = comment.discussion.id
-
-
# you cannot delete a comment if it has replies
-
# but if you could, you'd need to delete all the children, or rehome them
-
# Comment.where(parent_id: comment.id, parent_type: 'Comment').destroy_all
-
# Comment.where(parent_id: comment.id, parent_type: 'Comment').update(parent: comment.parent)
-
-
4
comment.destroy
-
4
RepairThreadWorker.perform_async(discussion_id)
-
end
-
-
1
def self.update(comment:, params:, actor:)
-
10
actor.ability.authorize! :update, comment
-
7
comment.edited_at = Time.zone.now
-
-
7
comment.assign_attributes_and_files(params)
-
7
return false unless comment.valid?
-
5
comment.save!
-
5
comment.update_versions_count
-
-
5
EventBus.broadcast('comment_update', comment, actor)
-
5
Events::CommentEdited.publish!(comment, actor)
-
end
-
end
-
class ContactMessageService
-
def self.create(contact_message:, actor:)
-
if contact_message.valid?
-
ContactMailer.contact_message(
-
contact_message.name,
-
contact_message.email,
-
contact_message.subject,
-
contact_message.message,
-
{
-
site: ENV['CANONICAL_HOST'],
-
form_type: 'Support',
-
user_id: actor.id
-
}.compact
-
).deliver_later
-
else
-
raise "failed to send a contact message. name: #{contact_message.name}, #{contact_message.email}, #{contact_message.subject}, #{contact_message.errors.to_s}"
-
end
-
end
-
end
-
class DemoService
-
def self.refill_queue
-
return unless ENV['FEATURES_DEMO_GROUPS']
-
demo = Demo.where('demo_handle is not null').last
-
return unless demo
-
-
# precache translations
-
AppConfig.locales['supported'].each do |locale|
-
TranslationService.translate_group_content!(demo.group, locale, true)
-
end
-
-
expected = ENV.fetch('FEATURES_DEMO_GROUPS_SIZE', 3)
-
remaining = Redis::List.new('demo_group_ids').value.size
-
-
(expected - remaining).times do
-
group = RecordCloner.new(recorded_at: demo.recorded_at).create_clone_group(demo.group)
-
Redis::List.new('demo_group_ids').push(group.id)
-
end
-
end
-
-
def self.take_demo(actor)
-
group = Group.find(Redis::List.new('demo_group_ids').shift)
-
group.creator = actor
-
group.subscription = Subscription.new(plan: 'demo', owner: actor)
-
group.add_member! actor
-
group.save!
-
-
if actor.locale != "en"
-
TranslationService.translate_group_content!(group, actor.locale)
-
end
-
-
EventBus.broadcast('demo_started', actor)
-
group
-
end
-
-
def self.ensure_queue
-
return unless ENV['FEATURES_DEMO_GROUPS']
-
existing_ids = Redis::List.new('demo_group_ids').value.select { |id| Group.where(id: id).exists? }
-
Redis::List.new('demo_group_ids').clear
-
Redis::List.new('demo_group_ids').unshift(*existing_ids) if existing_ids.any?
-
refill_queue
-
end
-
-
def self.generate_demo_groups
-
Demo.where("demo_handle IS NOT NULL").each do |template|
-
Group.where(handle: template.demo_handle).update_all(handle: nil)
-
RecordCloner.new(recorded_at: template.recorded_at)
-
.create_clone_group_for_public_demo(template.group, template.demo_handle)
-
end
-
end
-
end
-
1
class DiscussionReaderService
-
1
def self.redeem(discussion_reader: , actor: )
-
2
return unless DiscussionReader.redeemable_by(actor).where(id: discussion_reader.id).exists?
-
1
discussion_reader.update(user: actor, accepted_at: Time.zone.now)
-
rescue ActiveRecord::RecordNotUnique
-
DiscussionReader.find_by(discussion_id: discussion_reader.discussion_id,
-
user_id: actor.id).
-
update(inviter_id: discussion_reader.inviter_id,
-
accepted_at: Time.zone.now)
-
discussion_reader.destroy
-
end
-
end
-
1
class DiscussionService
-
1
def self.create(discussion:, actor:, params: {})
-
371
actor.ability.authorize!(:create, discussion)
-
-
367
UserInviter.authorize!(user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
model: discussion,
-
actor: actor)
-
-
365
discussion.author = actor
-
-
365
return false unless discussion.valid?
-
-
363
discussion.save!
-
-
363
DiscussionReader.for(user: actor, discussion: discussion)
-
.update(admin: true, guest: !discussion.group.present?, inviter_id: actor.id)
-
-
363
users = add_users(user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
discussion: discussion,
-
actor: actor)
-
-
363
EventBus.broadcast('discussion_create', discussion, actor)
-
363
Events::NewDiscussion.publish!(discussion: discussion,
-
recipient_user_ids: users.pluck(:id),
-
recipient_chatbot_ids: params[:recipient_chatbot_ids],
-
recipient_audience: params[:recipient_audience])
-
-
end
-
-
1
def self.update(discussion:, actor:, params:)
-
13
actor.ability.authorize! :update, discussion
-
-
12
UserInviter.authorize!(user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
model: discussion,
-
actor: actor)
-
-
-
12
discussion.assign_attributes_and_files(params.except(:group_id))
-
12
return false unless discussion.valid?
-
10
rearrange = discussion.max_depth_changed?
-
10
discussion.save!
-
-
10
discussion.update_versions_count
-
10
RepairThreadWorker.perform_async(discussion.id) if rearrange
-
-
10
users = add_users(discussion: discussion,
-
actor: actor,
-
user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience])
-
-
10
EventBus.broadcast('discussion_update', discussion, actor, params)
-
-
10
Events::DiscussionEdited.publish!(discussion: discussion,
-
actor: actor,
-
recipient_user_ids: users.pluck(:id),
-
recipient_chatbot_ids: params[:recipient_chatbot_ids],
-
recipient_audience: params[:recipient_audience],
-
recipient_message: params[:recipient_message])
-
end
-
-
1
def self.invite(discussion:, actor:, params:)
-
11
UserInviter.authorize!(user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
model: discussion,
-
actor: actor)
-
-
9
users = add_users(discussion: discussion,
-
actor: actor,
-
user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience])
-
-
9
Events::DiscussionAnnounced.publish!(discussion: discussion,
-
actor: actor,
-
recipient_user_ids: users.pluck(:id),
-
recipient_chatbot_ids: params[:recipient_chatbot_ids],
-
recipient_audience: params[:recipient_audience],
-
recipient_message: params[:recipient_message])
-
end
-
-
# def self.destroy(discussion:, actor:)
-
# actor.ability.authorize!(:destroy, discussion)
-
# discussion.discard!
-
# DestroyDiscussionWorker.perform_async(discussion.id)
-
# EventBus.broadcast('discussion_destroy', discussion, actor)
-
# end
-
-
1
def self.discard(discussion:, actor:)
-
actor.ability.authorize!(:discard, discussion)
-
discussion.update(discarded_at: Time.now, discarded_by: actor.id)
-
-
discussion.polls.update_all(discarded_at: Time.now, discarded_by: actor.id)
-
GenericWorker.perform_async('SearchService', 'reindex_by_discussion_id', discussion.id)
-
-
EventBus.broadcast('discussion_discard', discussion, actor)
-
discussion.created_event
-
end
-
-
1
def self.close(discussion:, actor:)
-
3
actor.ability.authorize! :update, discussion
-
1
discussion.update(closed_at: Time.now, closer_id: actor.id)
-
1
MessageChannelService.publish_models([discussion], group_id: discussion.group_id, user_id: actor.id)
-
end
-
-
1
def self.reopen(discussion:, actor:)
-
3
actor.ability.authorize! :update, discussion
-
1
discussion.update(closed_at: nil, closer_id: nil)
-
1
MessageChannelService.publish_models([discussion], group_id: discussion.group_id, user_id: actor.id)
-
end
-
-
1
def self.move(discussion:, params:, actor:)
-
9
source = discussion.group
-
9
destination = ModelLocator.new(:group, params).locate || NullGroup.new
-
9
destination.present? && actor.ability.authorize!(:move_discussions_to, destination)
-
8
actor.ability.authorize! :move, discussion
-
# discussion.add_admin!(actor)
-
-
7
discussion.update group: destination.presence, private: moved_discussion_privacy_for(discussion, destination)
-
8
discussion.polls.each { |poll| poll.update(group: destination.presence) }
-
7
ActiveStorage::Attachment.where(record: discussion.items.map(&:eventable).concat([discussion])).update_all(group_id: destination.id)
-
-
7
GenericWorker.perform_async('SearchService', 'reindex_by_discussion_id', discussion.id)
-
7
EventBus.broadcast('discussion_move', discussion, params, actor)
-
7
Events::DiscussionMoved.publish!(discussion, actor, source)
-
end
-
-
1
def self.pin(discussion:, actor:)
-
3
actor.ability.authorize! :pin, discussion
-
-
1
discussion.update(pinned_at: Time.now)
-
-
1
EventBus.broadcast('discussion_pin', discussion, actor)
-
end
-
-
1
def self.unpin(discussion:, actor:)
-
1
actor.ability.authorize! :pin, discussion
-
-
1
discussion.update(pinned_at: nil)
-
-
1
EventBus.broadcast('discussion_pin', discussion, actor)
-
end
-
-
1
def self.update_reader(discussion:, params:, actor:)
-
4
actor.ability.authorize! :show, discussion
-
2
reader = DiscussionReader.for(discussion: discussion, user: actor)
-
2
reader.update(params.slice(:volume))
-
2
Stance.joins(:poll).
-
where('polls.discussion_id': reader.discussion_id).
-
where(participant_id: actor.id).
-
update(params.slice(:volume))
-
-
2
EventBus.broadcast('discussion_update_reader', reader, params, actor)
-
end
-
-
1
def self.mark_as_seen(discussion:, actor:)
-
2
actor.ability.authorize! :mark_as_seen, discussion
-
1
reader = DiscussionReader.for_model(discussion, actor)
-
1
reader.viewed!
-
1
MessageChannelService.publish_models([reader.discussion], group_id: reader.discussion.group_id)
-
1
EventBus.broadcast('discussion_mark_as_seen', reader, actor)
-
end
-
-
1
def self.mark_as_read_simple_params(discussion_id, ranges, actor_id)
-
2
discussion = Discussion.find(discussion_id)
-
2
actor = User.find(actor_id)
-
2
mark_as_read(discussion: discussion, params: {ranges: ranges}, actor: actor)
-
end
-
-
1
def self.mark_as_read(discussion:, params:, actor:)
-
3
return unless actor.ability.can?(:mark_as_read, discussion)
-
3
RetryOnError.with_limit(2) do
-
3
sequence_ids = RangeSet.ranges_to_list(RangeSet.to_ranges(params[:ranges]))
-
3
NotificationService.viewed_events(actor_id: actor.id, discussion_id: discussion.id, sequence_ids: sequence_ids)
-
3
reader = DiscussionReader.for_model(discussion, actor)
-
3
reader.viewed!(params[:ranges])
-
3
EventBus.broadcast('discussion_mark_as_read', reader, actor)
-
end
-
end
-
-
1
def self.dismiss(discussion:, params:, actor:)
-
1
actor.ability.authorize! :dismiss, discussion
-
1
reader = DiscussionReader.for(user: actor, discussion: discussion)
-
1
reader.dismiss!
-
1
EventBus.broadcast('discussion_dismiss', reader, actor)
-
end
-
-
1
def self.recall(discussion:, params:, actor:)
-
1
actor.ability.authorize! :dismiss, discussion
-
1
reader = DiscussionReader.for(user: actor, discussion: discussion)
-
1
reader.recall!
-
1
EventBus.broadcast('discussion_recall', reader, actor)
-
end
-
-
1
def self.moved_discussion_privacy_for(discussion, destination)
-
7
case destination.discussion_privacy_options
-
1
when 'public_only' then false
-
5
when 'private_only' then true
-
1
else discussion.private
-
end
-
end
-
-
1
def self.mark_summary_email_as_read(user_id, time_start_i, time_finish_i)
-
1
user = User.find_by!(id: user_id)
-
1
time_start = Time.at(time_start_i).utc
-
1
time_finish = Time.at(time_finish_i).utc
-
1
time_range = time_start..time_finish
-
-
1
DiscussionQuery.visible_to(user: user, only_unread: true, or_public: false, or_subgroups: false).last_activity_after(time_start).each do |discussion|
-
1
RetryOnError.with_limit(2) do
-
1
sequence_ids = discussion.items.where("events.created_at": time_range).pluck(:sequence_id)
-
1
DiscussionReader.for(user: user, discussion: discussion).viewed!(sequence_ids)
-
end
-
end
-
end
-
-
1
def self.add_users(discussion:, actor:, user_ids:, emails:, audience:)
-
410
users = UserInviter.where_or_create!(actor: actor,
-
user_ids: user_ids,
-
emails: emails,
-
model: discussion,
-
audience: audience)
-
-
-
410
volumes = {}
-
410
Membership.where(group_id: discussion.group_id,
-
user_id: users.pluck(:id)).find_each do |m|
-
40
volumes[m.user_id] = m.volume
-
end
-
-
410
DiscussionReader.
-
where(discussion_id: discussion.id, user_id: users.map(&:id)).
-
where("revoked_at is not null").update_all(revoked_at: nil, revoker_id: nil)
-
-
410
new_discussion_readers = users.map do |user|
-
47
DiscussionReader.new(user: user,
-
discussion: discussion,
-
inviter: actor,
-
guest: !volumes.has_key?(user.id),
-
admin: !discussion.group_id,
-
volume: volumes[user.id] || user.default_membership_volume)
-
end
-
-
410
DiscussionReader.import(new_discussion_readers, on_duplicate_key_ignore: true)
-
-
410
discussion.update_members_count
-
410
users
-
end
-
-
1
def self.extract_link_preview_urls(discussion)
-
urls = discussion.link_previews.map { |lp| lp['url'] }
-
discussion.items.each do |event|
-
if event.eventable.present? && event.eventable.respond_to?(:link_previews)
-
urls.concat(event.eventable.link_previews.map {|lp| lp['url']})
-
end
-
end
-
urls.compact.uniq
-
end
-
end
-
class DiscussionTemplateService
-
def self.create(discussion_template:, actor:)
-
actor.ability.authorize! :create, discussion_template
-
-
discussion_template.assign_attributes(author: actor)
-
-
return false unless discussion_template.valid?
-
-
if discussion_template.key
-
discussion_template.group.hidden_discussion_templates += Array(discussion_template.key)
-
discussion_template.key = nil
-
end
-
-
discussion_template.save!
-
discussion_template
-
end
-
-
-
def self.update(discussion_template:, params:, actor:)
-
actor.ability.authorize! :update, discussion_template
-
-
discussion_template.assign_attributes_and_files(params.except(:group_id))
-
return false unless discussion_template.valid?
-
discussion_template.save!
-
-
discussion_template
-
end
-
-
def self.initial_templates(category)
-
names = {
-
board: ['discuss_a_topic', 'onboarding_to_loomio', 'approve_a_document', 'prepare_for_a_meeting', 'funding_decision'],
-
membership: ['discuss_a_topic', 'onboarding_to_loomio', 'share_links_and_info', 'decision_by_consensus', 'elect_a_governance_position'],
-
self_managing: ['discuss_a_topic', 'onboarding_to_loomio', 'advice_process', 'consent_process'],
-
other: ['discuss_a_topic', 'onboarding_to_loomio', 'approve_a_document', 'advice_process', 'consent_process'],
-
}.with_indifferent_access.fetch(category, ['blank'])
-
-
default_templates.filter { |dt| names.include? dt.key }
-
end
-
-
def self.default_templates
-
AppConfig.discussion_templates.map do |key, raw_attrs|
-
raw_attrs[:key] = key
-
attrs = {}
-
-
raw_attrs.each_pair do |key, value|
-
if key.match /_i18n$/
-
attrs[key.gsub(/_i18n$/, '')] = value.is_a?(Array) ? value.map {|v| I18n.t(v)} : I18n.t(value)
-
else
-
attrs[key] = value
-
end
-
end
-
-
DiscussionTemplate.new attrs
-
end.reverse
-
end
-
-
def self.create_public_templates
-
group = Group.find_or_create_by(handle: 'templates') do |group|
-
group.creator = User.helper_bot
-
group.name = 'Loomio Templates'
-
group.is_visible_to_public = false
-
group.logo.attach(io: URI.open(Rails.root.join('public/brand/icon_gold_256h.png')),
-
filename: 'loomiologo.png')
-
end
-
-
group.discussion_templates = default_templates.map do |dt|
-
dt.public = true
-
dt.author = User.helper_bot
-
dt
-
end
-
end
-
end
-
class DocumentService
-
def self.create(document:, actor:)
-
actor.ability.authorize! :create, document
-
-
document.assign_attributes(author: actor)
-
document.title ||= document.file_file_name
-
return unless document.valid?
-
document.save!
-
-
EventBus.broadcast 'document_create', document, actor
-
end
-
-
def self.update(document:, params:, actor:)
-
actor.ability.authorize! :update, document
-
-
document.assign_attributes(params.slice(:url, :title, :model_id, :model_type))
-
-
return unless document.valid?
-
document.save!
-
-
EventBus.broadcast 'document_update', document, params, actor
-
end
-
-
def self.destroy(document:, actor:)
-
actor.ability.authorize! :destroy, document
-
-
document.destroy
-
-
EventBus.broadcast 'document_destroy', document, actor
-
end
-
end
-
1
class EventService
-
1
def self.remove_from_thread(event:, actor:)
-
discussion = event.discussion
-
raise CanCan::AccessDenied.new unless event.kind == 'discussion_edited'
-
actor.ability.authorize! :remove_events, discussion
-
-
event.update(discussion_id: nil)
-
discussion.thread_item_destroyed!
-
GenericWorker.perform_async('SearchService', 'reindex_by_discussion_id', discussion.id)
-
-
EventBus.broadcast('event_remove_from_thread', event)
-
event
-
end
-
-
1
def self.move_comments(discussion:, actor:, params:)
-
# handle parent comments = events where parent_id is source.created_event.id
-
# move all events which are children of above parents (comment parent id untouched)
-
# handle any reply comments that don't have parent_id in given ids
-
-
7
ids = Array(params[:forked_event_ids]).compact
-
7
source = Event.find(ids.first).discussion
-
-
7
actor.ability.authorize! :move_comments, source
-
6
actor.ability.authorize! :move_comments, discussion
-
5
MoveCommentsWorker.perform_async(ids, source.id, discussion.id)
-
end
-
-
1
def self.repair_thread(discussion_id)
-
25
discussion = Discussion.find_by(id: discussion_id)
-
25
return unless discussion
-
-
# ensure discussion.created_event exists
-
25
unless discussion.created_event
-
Event.import [Event.new(kind: 'new_discussion',
-
user_id: discussion.author_id,
-
eventable_id: discussion.id,
-
eventable_type: "Discussion",
-
created_at: discussion.created_at)]
-
discussion.reload
-
end
-
-
25
Event.where(discussion_id: discussion_id, sequence_id: nil).order(:id).each(&:set_sequence_id!)
-
-
# rebuild ancestry of events based on eventable relationships
-
25
items = Event.where(discussion_id: discussion.id).order(:sequence_id)
-
25
items.update_all(parent_id: discussion.created_event.id, position: 0, position_key: nil, depth: 1)
-
25
items.reload.compact.each(&:set_parent_and_depth!)
-
-
25
parent_ids = items.pluck(:parent_id).compact.uniq
-
-
25
reset_child_positions(discussion.created_event.id, nil)
-
25
Event.where(id: parent_ids).order(:depth).each do |parent_event|
-
37
parent_event.reload
-
37
reset_child_positions(parent_event.id, parent_event.position_key)
-
end
-
-
25
ActiveRecord::Base.connection.execute(
-
"UPDATE events
-
SET descendant_count = (
-
SELECT count(descendants.id)
-
FROM events descendants
-
WHERE
-
descendants.discussion_id = events.discussion_id AND
-
descendants.id != events.id AND
-
descendants.position_key like CONCAT(events.position_key, '%')
-
), child_count = (
-
SELECT count(children.id) FROM events children
-
WHERE children.parent_id = events.id AND children.discussion_id IS NOT NULL
-
)
-
WHERE discussion_id = #{discussion_id.to_i}")
-
-
25
discussion.created_event.update_child_count
-
25
discussion.created_event.update_descendant_count
-
25
discussion.update_sequence_info!
-
-
# ensure all the discussion_readers have valid read_ranges values
-
25
DiscussionReader.where(discussion_id: discussion_id).each do |reader|
-
41
reader.update_columns(
-
read_ranges_string: RangeSet.serialize(
-
RangeSet.intersect_ranges(reader.read_ranges, discussion.ranges)
-
)
-
)
-
end
-
-
end
-
-
1
def self.reset_child_positions(parent_id, parent_position_key)
-
-
74
position_key_sql = if parent_position_key.nil?
-
60
"CONCAT(REPEAT('0',5-LENGTH(CONCAT(t.seq))), t.seq)"
-
else
-
14
"CONCAT('#{parent_position_key}-', CONCAT(REPEAT('0',5-LENGTH(CONCAT(t.seq) ) ), t.seq) )"
-
end
-
74
ActiveRecord::Base.connection.execute(
-
"UPDATE events SET position = t.seq, position_key = #{position_key_sql}
-
FROM (
-
SELECT id AS id, row_number() OVER(ORDER BY sequence_id) AS seq
-
FROM events
-
WHERE parent_id = #{parent_id}
-
AND discussion_id IS NOT NULL
-
) AS t
-
WHERE events.id = t.id and
-
events.position is distinct from t.seq")
-
74
SequenceService.drop_seq!('events_position', parent_id)
-
end
-
-
1
def self.repair_all_threads
-
Discussion.pluck(:id).each do |id|
-
RepairThreadWorker.perform_async(id)
-
end
-
end
-
end
-
1
class GroupExportService
-
RELATIONS = %w[
-
1
all_users
-
all_events
-
all_notifications
-
all_reactions
-
poll_templates
-
discussion_templates
-
memberships
-
membership_requests
-
discussions
-
exportable_polls
-
exportable_poll_options
-
exportable_outcomes
-
exportable_stances
-
exportable_stance_choices
-
discussion_readers
-
comments
-
]
-
-
JSON_PARAMS = {
-
1
groups: {except: [:token, :secret_token], methods: []},
-
comments: {except: [:secret_token]},
-
discussions: {except: [:secret_token]},
-
polls: {except: [:secret_token]},
-
outcomes: {except: [:secret_token]},
-
users: {except: [:encrypted_password,
-
:reset_password_token,
-
:email_api_key,
-
:reset_password_token,
-
:secret_token,
-
:unsubscribe_token] }
-
}.with_indifferent_access.freeze
-
-
BACK_REFERENCES = {
-
1
outcomes: {
-
events: %w[eventable]
-
},
-
comments: {
-
comments: %w[parent_id],
-
events: %w[eventable]
-
},
-
discussions: {
-
comments: %w[discussion_id],
-
discussion_readers: %w[discussion_id],
-
polls: %w[discussion_id],
-
events: %w[discussion_id eventable]
-
},
-
events: {
-
events: %w[parent_id],
-
notifications: %w[event_id]
-
},
-
groups: {
-
memberships: %w[group_id],
-
polls: %w[group_id],
-
discussions: %w[group_id],
-
tags: %w[group_id],
-
webhooks: %w[group_id],
-
events: %w[eventable],
-
groups: %w[parent_id],
-
poll_templates: %w[group_id],
-
discussion_templates: %w[group_id]
-
},
-
poll_options: {
-
stance_choices: %w[poll_option_id],
-
events: %w[eventable]
-
},
-
stances: {
-
stance_choices: %w[stance_id],
-
events: %w[eventable]
-
},
-
tasks: {
-
tasks_users: %w[task_id],
-
events: %w[eventable]
-
},
-
polls: {
-
stances: %w[poll_id],
-
poll_options: %w[poll_id],
-
outcomes: %w[poll_id],
-
events: %w[eventable]
-
},
-
users: {
-
events: %w[eventable user_id],
-
discussions: %w[author_id discarded_by],
-
attachments: %w[user_id],
-
comments: %w[user_id discarded_by] ,
-
discussion_readers: %w[user_id inviter_id],
-
groups: %w[creator_id],
-
membership_requests: %w[requestor_id responder_id],
-
memberships: %w[user_id inviter_id],
-
notifications: %w[user_id],
-
outcomes: %w[author_id],
-
polls: %w[author_id discarded_by],
-
reactions: %w[user_id],
-
stances: %w[participant_id inviter_id],
-
subscriptions: %w[owner_id],
-
tasks: %w[doer_id author_id],
-
tasks_users: %w[user_id],
-
versions: %w[whodunnit],
-
webhooks: %w[author_id]
-
}
-
}.with_indifferent_access.freeze
-
-
# export all the direct (invite-only) threads that people in a group have made
-
# TODO make this part of a normal export group process
-
1
def self.export_direct_threads(group_id)
-
group = Group.find(group_id)
-
group_ids = Group.find(group_id).id_and_subgroup_ids
-
author_ids = Membership.where(group_id: group_ids).pluck(:user_id).uniq
-
discussion_ids = Discussion.where(group_id: nil, author_id: author_ids).pluck(:id)
-
filename = "/tmp/#{DateTime.now.strftime("%Y-%m-%d_%H-%M-%S")}_invite-only-threads-for-#{group.name.parameterize}.json"
-
ids = Hash.new { |hash, key| hash[key] = [] }
-
File.open(filename, 'w') do |file|
-
Discussion.where(id: discussion_ids).each do |discussion|
-
puts_record(discussion, file, ids)
-
%w[exportable_polls
-
exportable_poll_options
-
exportable_outcomes
-
exportable_stances
-
exportable_stance_choices
-
all_reactions
-
comments
-
readers
-
items
-
discussion_readers].each do |relation|
-
discussion.send(relation).find_each(batch_size: 20000) do |record|
-
puts_record(record, file, ids)
-
end
-
end
-
-
attachments = [
-
discussion.files,
-
discussion.image_files,
-
discussion.comment_files,
-
discussion.comment_image_files,
-
discussion.poll_files,
-
discussion.poll_image_files,
-
discussion.outcome_files,
-
discussion.outcome_image_files
-
].compact.flatten.uniq.each do |attachment|
-
puts_attachment(attachment, file)
-
end
-
end
-
end
-
filename
-
end
-
-
1
def self.export(groups, group_name)
-
1
filename = export_filename_for(group_name)
-
4
ids = Hash.new { |hash, key| hash[key] = [] }
-
1
File.open(filename, 'w') do |file|
-
1
groups.each do |group|
-
1
puts_record(group, file, ids)
-
1
RELATIONS.each do |relation|
-
# puts "Exporting: #{relation}"
-
16
group.send(relation).find_each(batch_size: 20000) do |record|
-
4
puts_record(record, file, ids)
-
end
-
end
-
-
1
user_attachments = group.all_users.map(&:uploaded_avatar_attachment)
-
1
own_attachments = [group.cover_photo_attachment,
-
group.logo_attachment,
-
group.files_attachments,
-
group.image_files_attachments]
-
-
1
related_attachments = [group.comment_files,
-
group.comment_image_files,
-
group.discussion_files,
-
group.discussion_image_files,
-
group.poll_files,
-
group.poll_image_files,
-
group.outcome_files,
-
group.outcome_image_files,
-
group.subgroup_files,
-
group.subgroup_image_files,
-
group.subgroup_cover_photos,
-
group.subgroup_logos]
-
-
1
(user_attachments + own_attachments + related_attachments).
-
compact.flatten.uniq.each do |attachment|
-
puts_attachment(attachment, file)
-
end
-
end
-
end
-
1
filename
-
end
-
-
1
def self.export_filename_for(group_name)
-
1
"/tmp/#{DateTime.now.strftime("%Y-%m-%d_%H-%M-%S")}_#{group_name.parameterize}.json"
-
end
-
-
1
def self.puts_attachment(attachment, file)
-
download_path = Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
-
obj = {
-
id: attachment.id,
-
host: ENV['CANONICAL_HOST'],
-
record_type: attachment.record_type,
-
record_id: attachment.record_id,
-
name: attachment.name,
-
filename: attachment.filename,
-
content_type: attachment.content_type,
-
path: download_path,
-
url: "https://#{ENV['CANONICAL_HOST']}#{download_path}"
-
}
-
-
file.puts({table: 'attachments', record: obj}.to_json)
-
end
-
-
1
def self.puts_record(record, file, ids)
-
5
table = record.class.table_name
-
5
return if ids[table].include?(record.id)
-
5
ids[table] << record.id
-
5
file.puts({table: table, record: record.as_json(JSON_PARAMS[table])}.to_json)
-
end
-
-
1
def self.import(filename_or_url, reset_keys: false)
-
group_ids = []
-
migrate_ids = {}
-
-
if URI.parse(filename_or_url).class == URI::Generic
-
datas = File.open(filename_or_url).read.split("\n").map { |line| JSON.parse(line) }
-
else
-
datas = URI.parse(filename_or_url).read.split("\n").map { |line| JSON.parse(line) }
-
end
-
-
tables = datas.map{ |data| data['table'] }.uniq
-
-
ActiveRecord::Base.transaction do
-
#import the records, remember old with new ids
-
(tables - ['attachments']).each do |table|
-
migrate_ids[table] = {}
-
klass = table.classify.constantize
-
datas.each do |data|
-
next unless (data['table'] == table)
-
record = klass.new(data['record'])
-
-
if reset_keys && data['record'].has_key?('key')
-
record.key = nil
-
record.set_key
-
end
-
-
if data['record'].has_key?('secret_token')
-
record.secret_token = nil
-
end
-
-
if data['record'].has_key?('token')
-
record.token = klass.generate_unique_secure_token
-
end
-
-
if table == 'groups'
-
record.handle = GroupService.suggest_handle(name: record.handle, parent_handle: nil)
-
end
-
-
old_id = record.id
-
record.id = nil
-
result = klass.import([record], validate: false, on_duplicate_key_ignore: true)
-
if new_id = result.ids.map(&:to_i).first
-
migrate_ids[table][old_id] = new_id
-
else
-
# duplicate record exists
-
if table == 'users'
-
migrate_ids[table][old_id] = User.find_by(email: record.email).id
-
else
-
raise "failed to import #{table} record - conflict on unique column. handle that here"
-
end
-
end
-
end
-
end
-
-
# SIDEKIQ_REDIS_POOL.with_client do |client|
-
# client.set "last_migrate_ids", migrate_ids.to_json
-
# end
-
-
# rewrite references to old ids
-
(tables - ['attachments']).each do |table|
-
migrate_ids[table].each_pair do |old_id, new_id|
-
next unless BACK_REFERENCES.has_key?(table)
-
BACK_REFERENCES[table].each_pair do |ref_table, columns|
-
next unless migrate_ids[ref_table].present?
-
imported_ids = migrate_ids[ref_table].values
-
columns.each do |column|
-
if column == "eventable"
-
ref_table.classify.constantize.
-
where(id: imported_ids).
-
where(column+"_type" => table.classify, column+"_id" => old_id).
-
update_all(column+"_id" => new_id)
-
else
-
ref_table.classify.constantize.
-
where(id: imported_ids).
-
where(column => old_id).
-
update_all(column => new_id)
-
end
-
end
-
end
-
end
-
end
-
-
if tables.include?('attachments')
-
datas.each do |data|
-
next unless (data['table'] == 'attachments')
-
table = data['record']['record_type'].tableize
-
new_id = migrate_ids[table][data['record']['record_id']]
-
DownloadAttachmentWorker.perform_async(data['record'], new_id)
-
end
-
end
-
-
datas.each do |data|
-
if data['table'] == 'polls'
-
new_id = migrate_ids['polls'][data['record']['id']]
-
Poll.find(new_id).update_counts!
-
Poll.find(new_id).stances.each(&:update_option_scores!)
-
end
-
end
-
end
-
-
# SearchIndexWorker.new.perform(Discussion.where(group_id: group_ids).pluck(:id))
-
end
-
-
1
def self.download_attachment(record_data, new_id)
-
model = record_data['record_type'].classify.constantize.find(new_id)
-
file = URI.open(record_data['url'])
-
model.send(record_data['name']).attach(io: file, filename: record_data['filename'])
-
if model.respond_to?(:attachments)
-
model.update_attribute(:attachments, model.build_attachments)
-
end
-
file.close
-
end
-
end
-
1
module GroupService
-
1
def self.remote_cover_photo
-
# id like to use unsplash api but need to work out how to meet their attribution requirements
-
filename = %w[
-
8
cover1.jpg
-
cover2.jpg
-
cover3.jpg
-
cover4.jpg
-
cover5.jpg
-
cover6.jpg
-
cover7.jpg
-
cover8.jpg
-
cover9.jpg
-
cover10.jpg
-
cover11.jpg
-
cover12.jpg
-
cover13.jpg
-
cover14.jpg
-
].sample
-
8
Rails.root.join("public/theme/group_cover_photos/#{filename}")
-
end
-
-
1
def self.invite(group:, params:, actor:)
-
17
group_ids = if params[:invited_group_ids]
-
2
Array(params[:invited_group_ids]).map(&:to_i)
-
else
-
15
Array(group.id)
-
end
-
-
# restrict group_ids to a single organization
-
17
parent_group = Group.where(id: group_ids).first.parent_or_self
-
17
group_ids = parent_group.id_and_subgroup_ids & group_ids
-
-
17
UserInviter.authorize_add_members!(
-
parent_group: parent_group,
-
group_ids: group_ids,
-
emails: Array(params[:recipient_emails]),
-
user_ids: Array(params[:recipient_user_ids]),
-
actor: actor,
-
)
-
-
13
users = UserInviter.where_or_create!(
-
actor: actor,
-
model: group,
-
emails: params[:recipient_emails],
-
user_ids: params[:recipient_user_ids]
-
)
-
-
13
Group.where(id: group_ids).each do |g|
-
14
revoked_memberships = Membership.revoked.where(group_id: g.id, user_id: users.map(&:id))
-
14
revoked_memberships.update_all(
-
inviter_id: actor.id,
-
accepted_at: nil,
-
revoked_at: nil,
-
revoker_id: nil,
-
admin: false,
-
)
-
-
14
new_memberships = users.map do |user|
-
12
Membership.new(inviter: actor, user: user, group: g, volume: user.default_membership_volume)
-
end
-
-
14
Membership.import(new_memberships, on_duplicate_key_ignore: true)
-
-
# mark as accepted all invitiations to people who are already part of the org.
-
14
if g.parent
-
5
parent_members = g.parent.accepted_members.where(id: users.verified.pluck(:id))
-
5
Membership.pending.where(group_id: g.id,
-
user_id: parent_members.pluck(:id)).update_all(accepted_at: Time.now)
-
end
-
-
14
g.update_pending_memberships_count
-
14
g.update_memberships_count
-
14
GenericWorker.perform_async('PollService', 'group_members_added', g.id)
-
end
-
-
13
Events::MembershipCreated.publish!(
-
group: group,
-
actor: actor,
-
recipient_user_ids: users.pluck(:id),
-
recipient_message: params[:recipient_message]
-
)
-
-
13
Membership.active.where(group_id: group.id, user_id: users.pluck(:id))
-
end
-
-
1
def self.create(group:, actor: , skip_authorize: false)
-
8
actor.ability.authorize!(:create, group) unless skip_authorize
-
-
8
return false unless group.valid?
-
-
8
group.is_referral = actor.groups.size > 0
-
-
8
if group.is_parent?
-
8
url = remote_cover_photo
-
8
group.cover_photo.attach(io: URI.open(url), filename: File.basename(url))
-
8
group.creator = actor if actor.is_logged_in?
-
8
group.subscription = Subscription.new
-
end
-
-
8
group.save!
-
8
group.add_admin!(actor)
-
-
8
EventBus.broadcast('group_create', group, actor)
-
end
-
-
1
def self.update(group:, params:, actor:)
-
4
actor.ability.authorize! :update, group
-
-
4
group.assign_attributes_and_files(params)
-
4
group.group_privacy = params[:group_privacy] if params.has_key?(:group_privacy)
-
4
privacy_change = PrivacyChange.new(group)
-
-
4
return false unless group.valid?
-
4
group.save!
-
4
privacy_change.commit!
-
-
4
EventBus.broadcast('group_update', group, params, actor)
-
end
-
-
1
def self.destroy(group:, actor:)
-
3
actor.ability.authorize! :destroy, group
-
-
1
group.admins.each do |admin|
-
2
GroupMailer.destroy_warning(group.id, admin.id, actor.id).deliver_later
-
end
-
-
1
group.archive!
-
-
1
DestroyGroupWorker.perform_in(2.weeks, group.id)
-
1
EventBus.broadcast('group_destroy', group, actor)
-
end
-
-
1
def self.destroy_without_warning!(group_id)
-
Group.find(group_id).archive!
-
DestroyGroupWorker.perform_async(group_id)
-
end
-
-
1
def self.move(group:, parent:, actor:)
-
2
actor.ability.authorize! :move, group
-
1
group.update(handle: "#{parent.handle}-#{group.handle}") if group.handle?
-
1
group.update(parent: parent, subscription_id: nil)
-
1
EventBus.broadcast('group_move', group, parent, actor)
-
end
-
-
1
def self.export(group: , actor: )
-
1
actor.ability.authorize! :show, group
-
1
group_ids = actor.groups.where(id: group.all_groups).pluck(:id)
-
1
GroupExportWorker.perform_async(group_ids, group.name, actor.id)
-
end
-
-
1
def self.merge(source:, target:, actor:)
-
2
actor.ability.authorize! :merge, source
-
1
actor.ability.authorize! :merge, target
-
-
1
Group.transaction do
-
1
source.subgroups.update_all(parent_id: target.id)
-
1
source.discussions.update_all(group_id: target.id)
-
1
source.polls.update_all(group_id: target.id)
-
1
source.membership_requests.update_all(group_id: target.id)
-
1
source.group_identities.update_all(group_id: target.id)
-
1
source.memberships.where.not(user_id: target.member_ids).update_all(group_id: target.id)
-
1
source.destroy
-
end
-
end
-
-
1
def self.suggest_handle(name:, parent_handle:)
-
1321
attempt = 0
-
2643
while(Group.where(handle: generate_handle(name, parent_handle, attempt)).exists?) do
-
1
attempt += 1
-
end
-
1321
generate_handle(name, parent_handle, attempt)
-
end
-
-
1
private
-
-
1
def self.generate_handle(name, parent_handle, attempt)
-
2643
[parent_handle,
-
name,
-
3017
(attempt == 0) ? nil : attempt].compact.map{|t| t.to_s.strip.parameterize}.join('-')
-
end
-
end
-
1
class GroupService::PrivacyChange
-
1
attr_accessor :group
-
1
def initialize(group)
-
8
@group = group
-
8
@changed = group.changed
-
end
-
-
1
def commit!
-
8
@changed.each do |attribute|
-
14
case attribute
-
when 'is_visible_to_public'
-
4
if group.is_hidden_from_public?
-
2
make_discussions_private_in(group)
-
2
make_discussions_private_in(group.subgroups)
-
2
group.subgroups.each do |subgroup|
-
4
subgroup.group_privacy = 'closed'
-
4
subgroup.save!
-
end
-
end
-
when 'discussion_privacy_options'
-
4
case group.discussion_privacy_options
-
1
when 'private_only' then make_discussions_private_in(group)
-
3
when 'public_only' then make_discussions_public_in(group)
-
end
-
end
-
end
-
end
-
-
1
private
-
1
def make_discussions_private_in(group_or_groups)
-
5
Discussion.where(group_id: group_or_groups).update_all(private: true)
-
5
Array(group_or_groups).map(&:update_public_discussions_count)
-
end
-
-
1
def make_discussions_public_in(group_or_groups)
-
3
Discussion.where(group_id: group_or_groups).update_all(private: false)
-
3
Array(group_or_groups).map(&:update_public_discussions_count)
-
end
-
end
-
module LinkPreviewService
-
def self.fetch(url)
-
# require logged in user
-
# add rate limit of 100 per hour per user
-
response = HTTParty.get(url)
-
return nil if response.code != 200
-
doc = Nokogiri::HTML::Document.parse(response.body)
-
-
title = [doc.css('meta[property="og:title"]').attr('content')&.text,
-
doc.css('title').first&.text,
-
doc.css('h1').first&.text].reject(&:blank?).first
-
-
bad_titles = [/Google \w+: Sign-in/]
-
-
return nil if title.blank?
-
return nil if bad_titles.any? {|bt| bt.match?(title) }
-
-
description = [doc.css('meta[property="og:description"]').attr('content')&.text,
-
doc.css('meta[name="description"]').attr('content')&.text].reject(&:blank?).first
-
-
image = [doc.css('meta[property="og:image"]').attr('content')&.text,
-
doc.css('meta[name="og:image"]').attr('content')&.text,
-
doc.css('img[itemprop="image"]').attr('src')&.text,
-
doc.css('link[rel="image_src"]').attr('href')&.text].reject(&:blank?).first
-
-
{title: String(title).truncate(240),
-
description: String(description).truncate(240),
-
image: image,
-
url: url,
-
fit: 'contain',
-
align: 'center',
-
hostname: URI(url).host}
-
end
-
-
def self.fetch_urls(urls)
-
previews = []
-
threads = []
-
Array(urls).compact.reject {|u| BlockedDomain.where(name: URI(u).host).exists? }.each do |u|
-
# spawn a new thread for each url
-
threads << Thread.new do
-
previews.push fetch(u)
-
end
-
end
-
threads.each { |t| t.join }
-
previews.compact
-
rescue SocketError, URI::InvalidURIError, HTTParty::UnsupportedURIScheme, HTTParty::RedirectionTooDeep
-
[]
-
end
-
end
-
1
class LoginTokenService
-
1
def self.create(actor:, uri:)
-
12
return unless actor.presence
-
-
11
token = LoginToken.create!(redirect: (uri.path if uri&.host == ENV['CANONICAL_HOST']), user: actor)
-
-
11
UserMailer.login(actor.id, token.id).deliver_now
-
11
EventBus.broadcast('login_token_create', token, actor)
-
end
-
end
-
1
module MarkdownService
-
MARKDOWN_OPTIONS = [
-
1
no_intra_emphasis: true,
-
tables: true,
-
fenced_code_blocks: true,
-
autolink: true,
-
strikethrough: true,
-
space_after_headers: true,
-
superscript: true,
-
underline: true
-
].freeze
-
-
1
def self.render_markdown(text, format = 'md')
-
41
text.gsub!('](/rails/active_storage', ']('+lmo_asset_host+'/rails/active_storage')
-
41
text.gsub!('"/rails/active_storage', '"'+lmo_asset_host+'/rails/active_storage')
-
-
41
if format == "md"
-
41
text
-
else
-
ReverseMarkdown.convert(text)
-
end
-
end
-
-
1
def self.render_html(text)
-
2825
return '' if text.nil?
-
2821
renderer = LoomioMarkdown.new(filter_html: true, hard_wrap: true, link_attributes: {rel: "nofollow ugc noreferrer noopener", target: :_blank})
-
2821
Redcarpet::Markdown.new(renderer, *MARKDOWN_OPTIONS).render(text)
-
end
-
-
1
def self.render_rich_text(text, format = "md")
-
2485
return "" unless text
-
2485
text.gsub!('](/rails/active_storage', ']('+lmo_asset_host+'/rails/active_storage')
-
2485
text.gsub!('"/rails/active_storage', '"'+lmo_asset_host+'/rails/active_storage')
-
2485
if format == "md"
-
2440
MarkdownService.render_html(text)
-
else
-
45
replace_audios(replace_videos(replace_checkboxes(replace_iframes(text))))
-
end.html_safe
-
end
-
-
1
def self.render_plain_text(text, format = 'md')
-
15
return "" unless text
-
15
ActionController::Base.helpers.strip_tags(render_rich_text(text, format)).gsub(/(?:\n\r?|\r\n?)/, '<br>')
-
end
-
-
1
def self.replace_videos(str)
-
45
doc = Nokogiri::HTML5::DocumentFragment.parse(str)
-
45
doc.search("video[src]").each do |node|
-
node.replace("<p><a href='#{node['src']}'><img src='#{node['poster']}'><br>#{I18n.t('record_modal.watch_video')}</a></p>")
-
end
-
45
doc.to_s
-
end
-
-
1
def self.replace_audios(str)
-
45
doc = Nokogiri::HTML5::DocumentFragment.parse(str)
-
45
doc.search("audio[src]").each do |node|
-
node.replace("<p><a href='#{node['src']}'>#{I18n.t('record_modal.listen_to_audio')}</a></p>")
-
end
-
45
doc.to_s
-
end
-
-
1
def self.replace_iframes(str)
-
45
doc = Nokogiri::HTML5::DocumentFragment.parse(str)
-
45
doc.search("iframe[src]").each do |node|
-
begin
-
vi = VideoInfo.new(node['src'])
-
node.replace("<div><a href='#{vi.url}'><img src='#{vi.thumbnail}' /></a></div>")
-
rescue
-
node.replace("<a href='#{node['src']}'>#{node['src']}</a>")
-
end
-
end
-
45
doc.to_s
-
end
-
-
1
def self.replace_checkboxes(str)
-
45
frag = Nokogiri::HTML::DocumentFragment.parse(str)
-
45
frag.css('li[data-type="taskItem"]').each do |node|
-
if node['data-checked'] == 'true'
-
node.prepend_child '<div class="email-checkbox">✔️</div>'
-
else
-
node.prepend_child '<div class="email-checkbox"> </div>'
-
end
-
-
if node['data-due-on']
-
node.add_child '<span class="mailer-tag">📅 '+node['data-due-on']+'</div>'
-
end
-
end
-
45
frag.to_s
-
end
-
end
-
1
class MembershipRequestService
-
1
def self.create(membership_request:, actor:)
-
membership_request.requestor = actor
-
return false unless membership_request.valid?
-
actor.ability.authorize!(:create, membership_request)
-
-
membership_request.save!
-
Events::MembershipRequested.publish!(membership_request)
-
end
-
-
1
def self.approve(membership_request:, actor: )
-
2
actor.ability.authorize! :approve, membership_request
-
1
membership_request.approve!(actor)
-
1
Events::MembershipRequestApproved.publish!(membership_request.convert_to_membership!, actor)
-
end
-
-
1
def self.ignore(membership_request: , actor: )
-
2
actor.ability.authorize! :ignore, membership_request
-
1
membership_request.ignore!(actor)
-
end
-
end
-
1
class MembershipService
-
1
def self.redeem_if_pending!(membership)
-
5
redeem(membership: membership, actor: membership.user) if membership && membership.accepted_at.nil?
-
end
-
-
1
def self.redeem(membership:, actor:, notify: true)
-
11
raise Membership::InvitationAlreadyUsed.new(membership) if membership.accepted_at
-
-
# so we want to accept all the pending invitations this person has been sent within this org
-
# and we dont want any surprises if they already have some memberships.
-
# they may be accepting memberships send to a different email (unverified_user)
-
11
accepted_at = DateTime.now
-
-
11
invited_group_id = membership.group_id
-
11
existing_group_ids = Membership.where(user_id: actor.id).pluck(:group_id)
-
11
existing_accepted_group_ids = Membership.active.accepted.where(user_id: actor.id).pluck(:group_id)
-
11
invited_group_ids = Membership.pending.where(user_id: membership.user_id, group_id: membership.group.parent_or_self.id_and_subgroup_ids).pluck(:group_id)
-
-
# unrevoke any memberships the actor was just invited to
-
11
Membership.revoked
-
.where(user_id: actor.id, group_id: invited_group_ids)
-
.update(revoked_at: nil, revoker_id: nil, inviter_id: membership.inviter_id, accepted_at: accepted_at)
-
-
# ensure actor has accepted any existing pending memberships to this group
-
11
Membership.pending
-
.where(user_id: actor.id, group_id: invited_group_ids)
-
.update(accepted_at: accepted_at)
-
-
11
Membership.pending
-
11
.where(user_id: membership.user_id, group_id: (invited_group_ids - existing_group_ids))
-
.update(user_id: actor.id, accepted_at: accepted_at)
-
-
11
if (membership.user_id != actor.id)
-
8
Membership.where(user_id: membership.user_id, group_id: invited_group_ids).destroy_all
-
end
-
-
11
invited_group_ids.each do |group_id|
-
12
GenericWorker.perform_async('PollService', 'group_members_added', group_id)
-
end
-
-
# remove any existing guest access in these groups
-
11
DiscussionReader.joins(:discussion)
-
.where(user_id: actor.id, 'discussions.group_id': invited_group_ids, guest: true)
-
.update_all(guest: false, revoked_at: nil, revoker_id: nil)
-
-
11
Stance.joins(:poll)
-
.where(participant_id: actor.id, 'polls.group_id': invited_group_ids)
-
.update_all(guest: false)
-
-
# unrevoke any votes on active polls
-
11
Stance.joins(:poll)
-
.where(participant_id: actor.id)
-
.where('polls.group_id': invited_group_ids)
-
.where('stances.revoked_at is not null')
-
.where('polls.closed_at is null')
-
.update_all(revoked_at: nil, revoker_id: nil)
-
-
11
return if existing_accepted_group_ids.include?(invited_group_id)
-
9
membership = Membership.find_by!(group_id: invited_group_id, user_id: actor.id)
-
9
Events::InvitationAccepted.publish!(membership) if notify && membership.accepted_at
-
end
-
-
1
def self.revoke(membership:, actor:, revoked_at: DateTime.now)
-
4
actor.ability.authorize! :revoke, membership
-
-
# revoke guest access in case they were a guest before they were a member and it was not already cleaned up by redeem
-
4
revoke_by_id(
-
membership.group.id_and_subgroup_ids,
-
membership.user_id,
-
actor.id,
-
revoked_at,
-
)
-
-
4
EventBus.broadcast('membership_destroy', membership, actor)
-
end
-
-
1
def self.revoke_by_id(group_ids, user_id, actor_id, revoked_at = DateTime.now)
-
13
DiscussionReader
-
.joins(:discussion).guests
-
.where('discussions.group_id': group_ids, user_id: user_id)
-
.update_all(guest: false)
-
-
13
Stance.joins(:poll).guests
-
.where('polls.group_id': group_ids, participant_id: user_id)
-
.update_all(guest: false)
-
-
# remove them from active polls
-
13
group_ids.each do |group_id|
-
10
PollService.group_members_removed(group_id, user_id, actor_id, revoked_at)
-
end
-
-
# revoke the membership
-
13
Membership.active
-
.where(user_id: user_id, group_id: group_ids)
-
.update_all(revoked_at: revoked_at, revoker_id: actor_id)
-
-
13
Group.where(id: group_ids).map(&:update_memberships_count)
-
end
-
-
-
1
def self.update(membership:, params:, actor:)
-
1
actor.ability.authorize! :update, membership
-
-
1
membership.assign_attributes(params.slice(:title))
-
1
return false unless membership.valid?
-
1
membership.save!
-
-
1
update_user_titles_and_broadcast(membership.id)
-
-
1
EventBus.broadcast 'membership_update', membership, params, actor
-
end
-
-
1
def self.update_user_titles_and_broadcast(membership_id)
-
1
membership = Membership.find(membership_id)
-
-
1
user = membership.user
-
1
group = membership.group
-
-
1
return unless user && group
-
-
1
titles = (user.experiences['titles'] || {})
-
1
titles[group.id] = membership.title
-
1
user.experiences['titles'] = titles
-
1
user.save!
-
1
MessageChannelService.publish_models([user], serializer: AuthorSerializer, group_id: group.id)
-
end
-
-
1
def self.set_volume(membership:, params:, actor:)
-
3
actor.ability.authorize! :update, membership
-
3
val = Membership.volumes[params[:volume]]
-
3
if params[:apply_to_all]
-
1
group_ids = membership.group.parent_or_self.id_and_subgroup_ids
-
1
actor.memberships.where(group_id: group_ids).update_all(volume: val)
-
1
actor.discussion_readers.joins(:discussion).
-
where('discussions.group_id': group_ids).
-
update_all(volume: val)
-
1
Stance.joins(:poll).
-
where('polls.group_id': group_ids).
-
where(participant_id: actor.id).
-
update_all(volume: val)
-
else
-
2
membership.set_volume! params[:volume]
-
2
membership.discussion_readers.update_all(volume: val)
-
2
membership.stances.update_all(volume: val)
-
end
-
end
-
-
1
def self.resend(membership:, actor:)
-
3
actor.ability.authorize! :resend, membership
-
1
EventBus.broadcast 'membership_resend', membership, actor
-
1
Events::MembershipResent.publish!(membership, actor)
-
end
-
-
1
def self.make_admin(membership:, actor:)
-
actor.ability.authorize! :make_admin, membership
-
membership.update admin: true
-
Events::NewCoordinator.publish!(membership, actor)
-
end
-
-
1
def self.remove_admin(membership:, actor:)
-
actor.ability.authorize! :remove_admin, membership
-
membership.update admin: false
-
end
-
-
1
def self.join_group(group:, actor:)
-
actor.ability.authorize! :join, group
-
membership = group.add_member!(actor)
-
EventBus.broadcast('membership_join_group', group, actor)
-
Events::UserJoinedGroup.publish!(membership)
-
end
-
-
1
def self.add_users_to_group(users:, group:, inviter:)
-
inviter.ability.authorize!(:add_members, group)
-
group.add_members!(users, inviter: inviter).tap do |memberships|
-
Events::UserAddedToGroup.bulk_publish!(memberships, user: inviter)
-
end
-
end
-
-
1
def self.save_experience(membership:, actor:, params:)
-
2
actor.ability.authorize! :update, membership
-
1
membership.experienced!(params[:experience])
-
1
EventBus.broadcast('membership_save_experience', membership, actor, params)
-
end
-
end
-
1
class MergeUsersService
-
1
def self.send_merge_verification_email(actor:, target_email:)
-
actor.ability.authorize! :update, actor
-
target_user = User.active.find_by!(email: target_email)
-
prep_for_merge!(source_user: actor, target_user: target_user)
-
hash = MergeUsersService.build_merge_hash(source_user: actor, target_user: target_user)
-
UserMailer.merge_verification(source_user: actor, target_user: target_user, hash: hash).deliver_now
-
end
-
-
1
def self.prep_for_merge!(source_user:, target_user:)
-
2
source_user.update_attribute(:reset_password_token, User.generate_unique_secure_token)
-
2
target_user.update_attribute(:reset_password_token, User.generate_unique_secure_token)
-
end
-
-
1
def self.validate(source_user:, target_user:, hash:)
-
2
return false if source_user.id == target_user.id
-
2
hash == build_merge_hash(source_user: source_user, target_user: target_user)
-
end
-
-
1
def self.build_merge_hash(source_user:, target_user:)
-
3
sha1 = Digest::SHA1.new
-
3
sha1 << source_user.reset_password_token
-
3
sha1 << target_user.reset_password_token
-
3
sha1.hexdigest
-
end
-
end
-
1
class MessageChannelService
-
1
def self.publish_models(models, serializer: nil, scope: {}, root: nil, group_id: nil, user_id: nil)
-
2069
cache = RecordCache.for_collection(models, user_id)
-
2069
data = serialize_models(models, serializer: serializer, scope: scope.merge(cache: cache, current_user_id: user_id), root: root)
-
2069
publish_serialized_records(data, group_id: group_id, user_id: user_id)
-
end
-
-
1
def self.serialize_models(models, serializer: nil, scope: {}, root: nil)
-
2070
models = Array(models)
-
2070
return unless model = models.first
-
1816
serializer ||= model.is_a?(Event) ? EventSerializer : "#{model.class}Serializer".constantize
-
1816
root ||= model.is_a?(Event) ? 'events' : model.class.to_s.pluralize.downcase
-
1816
ActiveModel::ArraySerializer.new(models, scope: scope, each_serializer: serializer, root: root)
-
end
-
-
1
def self.publish_serialized_records(data, group_id: nil, user_id: nil)
-
2069
CACHE_REDIS_POOL.with do |client|
-
2069
room = "user-#{user_id}" if user_id
-
2069
room = "group-#{group_id}" if group_id
-
2069
data_str = data.as_json.as_json
-
2069
score = client.incr("/records/#{room}/score")
-
# puts "incrementing score:", room, score, data_str
-
2069
client.zadd("/records/#{room}", score, data_str.to_json)
-
2069
client.publish("/records", {room: room, records: data_str, score: score}.to_json)
-
2069
client.zremrangebyscore("/records/#{room}", "-inf", (score - 200))
-
end
-
end
-
-
1
def self.publish_system_notice(notice, reload = false)
-
CACHE_REDIS_POOL.with do |client|
-
client.publish("/system_notice", {version: Loomio::Version.current,
-
notice: notice,
-
reload: reload}.to_json)
-
end
-
end
-
end
-
module MigrateEventsService
-
def self.migrate_edited_eventable
-
Event.where(kind: ['poll_edited', 'discussion_edited'] ,
-
eventable_type: "PaperTrail::Version").find_each do |event|
-
version = event.eventable
-
event.update_columns(eventable_type: version.item_type,
-
eventable_id: version.item_id,
-
custom_fields: {version_id: version.id,
-
changed_keys: Hash(version.object_changes).keys})
-
-
-
end
-
Event.joins("LEFT OUTER JOIN polls on events.eventable_id = polls.id").where(eventable_type: "Poll").where("polls.id is null").destroy_all
-
end
-
-
def self.migrate_paperclip
-
models = [Group, User, Discussion, Comment, Poll, Stance, Outcome, Document]
-
-
models.each do |model|
-
attachments = model.column_names.map do |c|
-
if c =~ /(.+)_file_name$/
-
$1
-
end
-
end.compact
-
-
next if attachments.blank?
-
-
attachments.each do |attachment|
-
model.where("#{attachment}_file_name is not null").send("with_attached_#{attachment}").find_each.each do |instance|
-
if instance.send(attachment).attachment.nil?
-
puts "#{model.to_s} #{instance.id} " + instance.send("#{attachment}_file_name")
-
MigrateAttachmentWorker.perform_async(model.to_s, instance.id, attachment)
-
end
-
end
-
end
-
end
-
User.where("uploaded_avatar_file_name is not null").update_all(avatar_kind: "uploaded")
-
end
-
-
def self.rewrite_inline_images(host = nil)
-
ActiveStorage::Attachment.where(name: 'image_files').includes(:record).order('id desc').each do |attachment|
-
record = attachment.record
-
column_name = names[attachment.record_type]
-
next unless record[column_name].present?
-
-
host ||= Regexp.escape ENV['CANONICAL_HOST']
-
regex = /https:\/\/#{host}\/rails\/active_storage\/representations\/.*#{Regexp.escape URI.escape(attachment.filename.to_s)}/
-
-
if record[column_name].match?(regex)
-
path = Rails.application.routes.url_helpers.rails_representation_path(
-
attachment.representation(HasRichText::PREVIEW_OPTIONS),
-
only_path: true
-
)
-
puts "updating #{attachment.record_type} #{attachment.record_id}"
-
record.update_columns(column_name => record[column_name].gsub(regex, path))
-
end
-
end
-
end
-
-
def self.rewrite_attachment_links
-
names.each_pair do |type, col|
-
type.constantize.where('attachments != ?', '[]').with_attached_files.each do |record|
-
record.update_columns(attachments: record.build_attachments)
-
end
-
end
-
end
-
-
def self.names
-
names = {
-
'Discussion' => 'description',
-
'Comment' => 'body',
-
'Poll' => 'details',
-
'Outcome' => 'statement',
-
'Stance' => 'reason',
-
'User' => 'short_bio',
-
'Group' => 'description',
-
}
-
end
-
end
-
class MigrateGuestsService
-
def self.migrate!
-
DiscussionReader.joins(:discussion).where('discussions.group_id': nil).update_all(guest: true)
-
Stance.joins(:poll).where('polls.group_id': nil).update_all(guest: true)
-
Group.order('discussions_count desc').pluck(:id).each do |id|
-
MigrateGuestOnDiscussionReadersAndStances.perform_async(id)
-
end
-
end
-
end
-
class MybbService
-
def self.convert_text(text)
-
text.gsub('[hr]', '<hr>').gsub(/\[quote="[^\]]+\]/, '<blockquote>').gsub('[/quote]', '</blockquote>')
-
end
-
-
# note, I edited the posts.json and deleted the first couple of lines and the last couple too.
-
# so I could read the file line by line and just consider each line a post
-
# thats why I chop the trailing commas
-
def self.import(filename, group_id)
-
user_ids = {}
-
discussion_ids = {}
-
comment_ids = {}
-
-
posts = URI.open(filename, 'r').map do |line|
-
line.chop! if line.ends_with?("\n")
-
line.chop! if line.ends_with?(',')
-
JSON.parse(line)
-
rescue
-
byebug
-
end
-
-
ActiveRecord::Base.transaction do
-
# create discussions
-
posts.sort_by {|post| post['replyto'].to_i }.each do |post|
-
# create user if not exists
-
unless user_ids[post['uid']]
-
email = "#{post['username'].parameterize.gsub('-','')}@mybb.example.com"
-
u = User.find_by(email: email) || u = User.create!(
-
name: post['username'],
-
email: email
-
)
-
user_ids[post['uid']] = u.id
-
end
-
-
# create discussion if not exists
-
if !discussion_ids[post['tid']]
-
d = Discussion.create!(
-
group_id: group_id,
-
author_id: user_ids[post['uid']],
-
title: post['subject'],
-
description: convert_text(post['message']),
-
created_at: Time.at(post['dateline'].to_i),
-
updated_at: Time.at(post['dateline'].to_i)
-
)
-
d.create_missing_created_event!
-
discussion_ids[post['tid']] = d.id
-
else
-
if post['message'].present?
-
parent = Comment.find_by(id: post['replyto'].to_i)
-
comment = Comment.create!(
-
parent_id: comment_ids[parent&.id],
-
discussion_id: discussion_ids[post['tid']],
-
user_id: user_ids[post['uid']],
-
created_at: Time.at(post['dateline'].to_i),
-
updated_at: Time.at(post['dateline'].to_i),
-
body: convert_text(post['message'])
-
)
-
comment_ids[post['pid']] = comment.id
-
Event.create!(
-
user_id: user_ids[post['uid']],
-
discussion_id: discussion_ids[post['tid']],
-
kind: "new_comment",
-
eventable: comment,
-
created_at: Time.at(post['dateline'].to_i),
-
updated_at: Time.at(post['dateline'].to_i)
-
)
-
end
-
end
-
end
-
end
-
ids = Discussion.where(group_id: group_id).pluck(:id)
-
ids.each {|id| EventService.repair_thread(id)}
-
# SearchIndexWorker.new.perform(ids)
-
end
-
end
-
1
class NewsletterService
-
1
LISTMONK_URL = ENV.fetch('LISTMONK_URL', '')
-
1
LISTMONK_USERNAME = ENV.fetch('LISTMONK_USERNAME', nil)
-
1
LISTMONK_PASSWORD = ENV.fetch('LISTMONK_PASSWORD', nil)
-
1
LISTMONK_LIST_ID = ENV.fetch('LISTMONK_LIST_ID', 3)
-
-
1
def self.enabled?
-
6
LISTMONK_URL.starts_with?('http') && LISTMONK_USERNAME && LISTMONK_PASSWORD && LISTMONK_LIST_ID
-
end
-
-
1
def self.subscribe(name, email)
-
return unless enabled?
-
-
HTTParty.post(
-
"#{LISTMONK_URL}/api/subscribers",
-
{
-
basic_auth: auth,
-
headers: { 'Content-Type' => 'application/json' },
-
body: {
-
email: parse_email(email),
-
name: name,
-
status: 'enabled',
-
lists: [LISTMONK_LIST_ID.to_i],
-
preconfirm_subscriptions: true,
-
}.to_json,
-
# :debug_output => $stdout
-
}
-
)
-
end
-
-
1
def self.unsubscribe(email)
-
6
return unless enabled?
-
-
response = HTTParty.get(
-
"#{LISTMONK_URL}/api/subscribers",
-
basic_auth: auth,
-
query: {
-
query: "subscribers.email LIKE '#{parse_email(email)}'"
-
}
-
)
-
-
subscriber_id = response.dig('data', 'results', 0, 'id')
-
-
return unless subscriber_id.present?
-
-
HTTParty.delete(
-
"#{LISTMONK_URL}/api/subscribers/#{subscriber_id}",
-
basic_auth: auth
-
)
-
end
-
-
1
def self.auth
-
{username: LISTMONK_USERNAME, password: LISTMONK_PASSWORD}
-
end
-
-
1
def self.parse_email(email)
-
ret = email.to_s.scan(AppConfig::EMAIL_REGEX).uniq.first
-
raise "invalid email #{email}" unless ret.present?
-
ret
-
end
-
end
-
1
class NotificationService
-
1
def self.mark_as_read(eventable_type, eventable_id, actor_id)
-
251
ids = Notification.joins(:event)
-
.where(user_id: actor_id, viewed: false)
-
.where('events.eventable_type': eventable_type, 'events.eventable_id': eventable_id).pluck(:id)
-
-
251
notifications = Notification.where(user_id: actor_id, id: ids, 'viewed': false)
-
251
notifications.update_all(viewed: true)
-
251
notifications.reload
-
251
MessageChannelService.publish_models(notifications, user_id: actor_id)
-
end
-
-
1
def self.viewed_events(actor_id:, discussion_id: , sequence_ids: )
-
3
event_ids = []
-
-
3
events = Event.includes(:eventable).where(discussion_id: discussion_id, sequence_id: sequence_ids)
-
-
3
reactions = Reaction.where(reactable: events.map(&:eventable))
-
3
event_ids.concat Event.where(eventable: reactions).pluck(:id)
-
-
3
eventable_ids = {}
-
-
3
%w[Comment Discussion Poll Stance Outcome].each do |type|
-
15
eventable_ids[type] = Event.where(
-
discussion_id: discussion_id,
-
sequence_id: sequence_ids,
-
eventable_type: type).pluck(:eventable_id)
-
end
-
-
-
3
eventable_ids.each_pair do |type, ids|
-
15
event_ids.concat Notification.joins(:event).where(
-
user_id: actor_id,
-
viewed: false,
-
'events.eventable_type': type,
-
'events.eventable_id': ids).pluck('events.id')
-
end
-
-
3
notifications = Notification.where(user_id: actor_id).
-
where(event_id: event_ids.uniq).
-
where('viewed': false)
-
3
notifications.update_all(viewed: true)
-
3
notifications.reload
-
3
MessageChannelService.publish_models(notifications, user_id: actor_id)
-
end
-
-
1
def self.viewed(user:)
-
user.notifications.where(viewed: false).update_all(viewed: true)
-
notifications = user.notifications.includes(:actor, :user).order(created_at: :desc).limit(30)
-
-
# alert clients (say, user's other tabs) that notifications have been read
-
MessageChannelService.publish_models(notifications, user_id: user.id)
-
end
-
end
-
1
class OutcomeService
-
1
def self.invite(outcome:, actor:, params:)
-
17
actor.ability.authorize! :announce, outcome
-
-
16
UserInviter.authorize!(user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
model: outcome,
-
actor: actor)
-
-
15
users = UserInviter.where_or_create!(actor: actor,
-
model: outcome,
-
emails: params[:recipient_emails],
-
user_ids: params[:recipient_user_ids],
-
audience: params[:recipient_audience],
-
include_actor: params[:include_actor].present?)
-
-
15
Events::OutcomeAnnounced.publish!(outcome, actor, users.pluck(:id), params[:recipient_audience])
-
15
users
-
end
-
-
1
def self.create(outcome:, actor:, params: {})
-
29
actor.ability.authorize! :create, outcome
-
-
24
UserInviter.authorize!(user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
model: outcome,
-
actor: actor)
-
-
24
outcome.assign_attributes(author: actor)
-
24
return false unless outcome.valid?
-
21
outcome.poll.outcomes.update_all(latest: false)
-
-
21
outcome.save!
-
-
21
users = UserInviter.where_or_create!(actor: actor,
-
emails: params[:recipient_emails],
-
user_ids: params[:recipient_user_ids],
-
model: outcome,
-
audience: params[:recipient_audience],
-
include_actor: params[:include_actor].present?)
-
-
21
EventBus.broadcast 'outcome_create', outcome, actor
-
-
21
Events::OutcomeCreated.publish!(outcome: outcome,
-
recipient_user_ids: users.pluck(:id),
-
recipient_chatbot_ids: params[:recipient_chatbot_ids],
-
recipient_audience: params[:recipient_audience])
-
end
-
-
1
def self.update(outcome:, actor:, params: {})
-
5
actor.ability.authorize! :update, outcome
-
-
2
UserInviter.authorize!(user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
model: outcome,
-
actor: actor)
-
-
2
outcome.assign_attributes_and_files(params.slice(:review_on, :statement, :statement_format, :event_summary, :event_location, :files, :image_files, :link_previews, :poll_option_id))
-
2
return false unless outcome.valid?
-
-
1
outcome.save!
-
1
outcome.update_versions_count
-
-
1
users = UserInviter.where_or_create!(actor: actor,
-
emails: params[:recipient_emails],
-
user_ids: params[:recipient_user_ids],
-
model: outcome,
-
audience: params[:recipient_audience],
-
include_actor: params[:include_actor].present?)
-
-
1
EventBus.broadcast 'outcome_update', outcome, actor
-
-
1
Events::OutcomeUpdated.publish!(outcome: outcome,
-
actor: actor,
-
recipient_user_ids: users.pluck(:id),
-
recipient_chatbot_ids: params[:recipient_chatbot_ids],
-
recipient_audience: params[:recipient_audience])
-
end
-
-
1
def self.publish_review_due
-
3
Outcome.review_due_not_published(Date.today).each do |outcome|
-
1
Events::OutcomeReviewDue.publish!(outcome)
-
end
-
end
-
-
end
-
1
class PollService
-
1
def self.create(poll:, actor:, params: {})
-
172
actor.ability.authorize! :create, poll
-
-
167
poll.assign_attributes(author: actor)
-
167
poll.prioritise_poll_options!
-
-
167
return false unless poll.valid?
-
165
poll.save!
-
165
poll.update_counts!
-
-
165
if !poll.specified_voters_only
-
165
stances = create_stances(poll: poll, actor: actor, include_actor: true, audience: 'group')
-
else
-
stances = Stance.none
-
end
-
-
165
user_ids = params[:notify_recipients] ? stances.pluck(:participant_id) : []
-
-
165
EventBus.broadcast('poll_create', poll, actor)
-
165
Events::PollCreated.publish!(poll, actor, recipient_user_ids: user_ids - [actor.id])
-
end
-
-
1
def self.update(poll:, params:, actor:)
-
10
actor.ability.authorize! :update, poll
-
-
8
UserInviter.authorize!(
-
user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
model: poll,
-
actor: actor
-
)
-
-
8
poll.assign_attributes_and_files(params.except(:poll_type, :discussion_id, :poll_template_id, :poll_template_key))
-
-
# check again, because the group id could be updated to a untrusted group
-
8
actor.ability.authorize! :update, poll
-
-
7
poll.prioritise_poll_options!
-
-
7
return false unless poll.valid?
-
-
6
poll.save!
-
6
poll.update_counts!
-
6
GenericWorker.perform_async('SearchService', 'reindex_by_poll_id', poll.id)
-
-
6
users = UserInviter.where_or_create!(
-
actor: actor,
-
user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
model: poll
-
)
-
-
6
EventBus.broadcast('poll_update', poll, actor)
-
-
6
Events::PollEdited.publish!(
-
poll: poll,
-
actor: actor,
-
recipient_user_ids: users.pluck(:id),
-
recipient_chatbot_ids: params[:recipient_chatbot_ids],
-
recipient_audience: params[:recipient_audience],
-
recipient_message: params[:recipient_message]
-
)
-
end
-
-
1
def self.invite(poll:, actor:, params:)
-
50
UserInviter.authorize!(
-
user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
model: poll,
-
actor: actor,
-
)
-
-
48
if poll.discussion
-
28
DiscussionService.add_users(
-
discussion: poll.discussion,
-
actor: actor,
-
user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
audience: params[:recipient_audience],
-
)
-
end
-
-
48
stances = create_stances(
-
poll: poll, actor: actor,
-
user_ids: params[:recipient_user_ids],
-
emails: params[:recipient_emails],
-
include_actor: params[:include_actor],
-
audience: params[:recipient_audience]
-
)
-
-
48
if params[:notify_recipients]
-
2
Events::PollAnnounced.publish!(
-
poll: poll,
-
actor: actor,
-
stances: stances,
-
recipient_user_ids: params[:recipient_user_ids],
-
recipient_chatbot_ids: params[:recipient_chatbot_ids],
-
recipient_audience: params[:recipient_audience],
-
recipient_message: params[:recipient_message],
-
)
-
end
-
-
48
stances
-
end
-
-
1
def self.remind(poll:, actor:, params:)
-
actor.ability.authorize! :remind, poll
-
-
users = UserInviter.where_existing(
-
user_ids: params[:recipient_user_ids],
-
audience: params[:recipient_audience],
-
model: poll,
-
actor: actor
-
)
-
-
Events::PollReminder.publish!(
-
poll: poll,
-
actor: actor,
-
recipient_user_ids: users.pluck(:id),
-
recipient_chatbot_ids: params[:recipient_chatbot_ids],
-
recipient_audience: params[:recipient_audience],
-
recipient_message: params[:recipient_message]
-
)
-
end
-
-
1
def self.create_stances(poll:, actor:, user_ids: [], emails: [], audience: nil, include_actor: false)
-
748
existing_voter_ids = Stance.latest.where(poll_id: poll.id).pluck(:participant_id)
-
-
748
users = UserInviter.where_or_create!(
-
actor: actor,
-
model: poll,
-
user_ids: user_ids,
-
audience: audience,
-
include_actor: include_actor,
-
emails: emails
-
).where.not(id: existing_voter_ids)
-
-
748
volumes = {}
-
748
group_member_ids = (poll.group || NullGroup.new).member_ids
-
-
748
if poll.discussion_id
-
676
DiscussionReader.active.where(
-
discussion_id: poll.discussion_id,
-
user_id: users.pluck(:id),
-
).find_each do |dr|
-
116
volumes[dr.user_id] = dr.volume
-
end
-
end
-
-
748
if poll.group_id
-
742
Membership.active.where(
-
group_id: poll.group_id,
-
user_id: users.pluck(:id),
-
).find_each do |m|
-
1973
volumes[m.user_id] = m.volume unless volumes.has_key? m.user_id
-
end
-
end
-
-
748
reinvited_user_ids = Stance.revoked.where(poll_id: poll.id).pluck(:participant_id) & users.pluck(:id)
-
-
748
Stance.where(poll_id: poll.id, participant_id: reinvited_user_ids).each do |stance|
-
1
stance.update(revoked_at: nil, revoker_id: nil, inviter_id: actor.id, admin: false)
-
end
-
-
748
new_stances = users.where.not(id: reinvited_user_ids).map do |user|
-
1980
Stance.new(
-
participant: user,
-
poll: poll,
-
inviter: actor,
-
guest: !group_member_ids.include?(user.id),
-
volume: volumes[user.id] || user.default_membership_volume,
-
latest: true,
-
reason_format: user.default_format,
-
created_at: Time.zone.now
-
)
-
end
-
-
748
Stance.import(new_stances, on_duplicate_key_ignore: true)
-
-
748
poll.reset_latest_stances!
-
748
poll.update_counts!
-
-
748
Stance.where(participant_id: users.pluck(:id), poll_id: poll.id, latest: true)
-
end
-
-
1
def self.discard(poll:, actor:)
-
2
actor.ability.authorize!(:destroy, poll)
-
-
1
poll.update(discarded_at: Time.now, discarded_by: actor.id)
-
1
Event.where(kind: ["stance_created", "stance_updated"], eventable_id: poll.stances.pluck(:id)).update_all(discussion_id: nil)
-
1
poll.created_event.update!(user_id: nil, child_count: 0, pinned: false)
-
1
MessageChannelService.publish_models([poll.created_event], scope: {current_user: actor, current_user_id: actor.id}, group_id: poll.group_id)
-
1
poll.created_event
-
end
-
-
1
def self.close(poll:, actor:)
-
9
actor.ability.authorize! :close, poll
-
6
do_closing_work(poll: poll)
-
6
Events::PollClosedByUser.publish!(poll, actor)
-
end
-
-
1
def self.reopen(poll:, params:, actor:)
-
3
actor.ability.authorize! :reopen, poll
-
-
1
poll.assign_attributes(closing_at: params[:closing_at], closed_at: nil)
-
1
return false unless poll.valid?
-
-
1
poll.save!
-
-
1
EventBus.broadcast('poll_reopen', poll, actor)
-
1
Events::PollReopened.publish!(poll, actor)
-
end
-
-
1
def self.publish_closing_soon
-
28
hour_start = 1.day.from_now.at_beginning_of_hour
-
28
hour_finish = hour_start + 1.hour
-
28
this_hour_tomorrow = hour_start..hour_finish
-
28
Poll.closing_soon_not_published(this_hour_tomorrow).each do |poll|
-
28
Events::PollClosingSoon.publish!(poll)
-
end
-
end
-
-
1
def self.group_members_added(group_id)
-
5869
member_ids = Group.find(group_id).members.humans.pluck(:id)
-
5869
Poll.active.where(group_id: group_id, specified_voters_only: false).each do |poll|
-
521
revoked_user_ids = poll.stances.revoked.pluck(:participant_id).uniq
-
521
PollService.create_stances(
-
poll: poll,
-
actor: poll.author,
-
521
user_ids: (member_ids - poll.voter_ids) - revoked_user_ids
-
)
-
521
poll.update_counts!
-
end
-
end
-
-
1
def self.group_members_removed(group_id, removed_user_ids, actor_id, revoked_at)
-
10
Poll.active.where(group_id: group_id).each do |poll|
-
2
Stance.where(
-
poll_id: poll.id,
-
revoked_at: nil,
-
participant_id: Array(removed_user_ids),
-
).update_all(revoked_at: revoked_at, revoker_id: actor_id)
-
2
poll.update_counts!
-
end
-
end
-
-
1
def self.expire_lapsed_polls
-
17
Poll.lapsed_but_not_closed.each do |poll|
-
15
CloseExpiredPollWorker.perform_async(poll.id)
-
end
-
end
-
-
1
def self.do_closing_work(poll:)
-
21
return if poll.closed_at
-
21
poll.stances.update_all(participant_id: nil) if poll.anonymous
-
21
if poll.discussion_id && poll.hide_results == 'until_closed'
-
1
stance_ids = poll.stances.latest.reject(&:body_is_blank?).map(&:id)
-
1
Event.where(kind: 'stance_created', eventable_id: stance_ids, discussion_id: nil).update_all(discussion_id: poll.discussion_id)
-
1
EventService.repair_thread(poll.discussion_id)
-
end
-
21
poll.update_attribute(:closed_at, Time.now)
-
21
GenericWorker.perform_async('SearchService', 'reindex_by_poll_id', poll.id)
-
end
-
-
# def self.destroy(poll:, actor:)
-
# actor.ability.authorize! :destroy, poll
-
# poll.destroy
-
#
-
# EventBus.broadcast('poll_destroy', poll, actor)
-
# end
-
-
1
def self.add_to_thread(poll:, params:, actor:)
-
1
discussion = Discussion.find(params[:discussion_id])
-
1
actor.ability.authorize! :update, poll
-
1
actor.ability.authorize! :update, discussion
-
1
ActiveRecord::Base.transaction do
-
1
poll.update(discussion_id: discussion.id, group_id: discussion.group.id)
-
1
event = poll.created_event
-
1
event.discussion_id = discussion.id
-
1
event.parent_id = discussion.created_event.id
-
1
event.pinned = true
-
1
event.set_sequences
-
1
event.save
-
1
poll.created_event.update_sequence_info!
-
end
-
-
1
if (poll.closed? || poll.hide_results != 'until_closed')
-
1
stance_ids = poll.stances.latest.reject(&:body_is_blank?).map(&:id)
-
1
Event.where(kind: 'stance_created', eventable_id: stance_ids).update_all(discussion_id: poll.discussion_id)
-
1
EventService.repair_thread(poll.discussion_id)
-
end
-
-
1
GenericWorker.perform_async('SearchService', 'reindex_by_discussion_id', discussion.id)
-
-
1
poll.created_event
-
end
-
-
1
def self.calculate_results(poll, poll_options)
-
648
sorted_poll_options = case poll.order_results_by
-
when 'priority'
-
1016
poll_options.sort_by {|o| o.priority }
-
else
-
# when 'total_score_desc'
-
1502
poll_options.sort_by {|o| -(o.total_score)}
-
end
-
-
648
l = sorted_poll_options.each_with_index.map do |option, index|
-
1870
option_name = poll.poll_option_name_format == 'i18n' ? "poll_#{poll.poll_type}_options."+option.name : option.name
-
{
-
1870
id: option.id,
-
poll_id: option.poll_id,
-
name: option_name,
-
name_format: poll.poll_option_name_format,
-
icon: option.icon,
-
rank: index+1,
-
score: option.total_score,
-
1870
target_percent: ((option.icon == 'agree') && (poll.agree_target.to_i > 0)) ? ((option.total_score.to_f / poll.agree_target.to_f) * 100) : 0,
-
1870
score_percent: poll.total_score > 0 ? ((option.total_score.to_f / poll.total_score.to_f) * 100) : 0,
-
1870
max_score_percent: poll.total_score > 0 ? ((option.total_score.to_f / poll.stance_counts.max.to_f) * 100) : 0,
-
1870
voter_percent: poll.voters_count > 0 ? ((option.voter_count.to_f / poll.voters_count.to_f) * 100) : 0,
-
average: option.average_score,
-
voter_scores: option.voter_scores,
-
voter_ids: option.voter_ids.take(500),
-
voter_count: option.voter_count,
-
color: option.color
-
}.with_indifferent_access.freeze
-
end
-
648
if poll.results_include_undecided
-
616
l.push({
-
id: 0,
-
poll_id: poll.id,
-
name: 'poll_common_votes_panel.undecided',
-
name_format: 'i18n',
-
rank: nil,
-
score: 0,
-
score_percent: 0,
-
max_score_percent: 0,
-
616
target_percent: poll.voters_count > 0 ? (poll.undecided_voters_count.to_f / poll.voters_count.to_f * 100) : 0,
-
616
voter_percent: poll.voters_count > 0 ? (poll.undecided_voters_count.to_f / poll.voters_count.to_f * 100) : 0,
-
average: 0,
-
voter_scores: {},
-
voter_ids: poll.undecided_voters.map(&:id).take(500),
-
voter_count: poll.undecided_voters_count,
-
color: '#BBBBBB'
-
}.with_indifferent_access.freeze)
-
end
-
648
l
-
end
-
end
-
class PollTemplateService
-
def self.group_templates(group:)
-
group.poll_templates.to_a.concat(
-
default_templates.map do |template|
-
template.position = group.poll_template_positions.fetch(template.key, 999)
-
template.group_id = group.id
-
template.discarded_at = DateTime.now if group.hidden_poll_templates.include?(template.key)
-
template
-
end
-
)
-
end
-
-
def self.default_templates
-
AppConfig.poll_templates.map do |key, raw_attrs|
-
raw_attrs[:key] = key
-
attrs = {}
-
-
AppConfig.poll_types[raw_attrs['poll_type']]['defaults'].each_pair do |key, value|
-
if key.match /_i18n$/
-
attrs[key.gsub(/_i18n$/, '')] = value.is_a?(Array) ? value.map {|v| I18n.t(v)} : I18n.t(value)
-
else
-
attrs[key] = value
-
end
-
end
-
-
raw_attrs.each_pair do |key, value|
-
if key.match /_i18n$/
-
attrs[key.gsub(/_i18n$/, '')] = value.is_a?(Array) ? value.map {|v| I18n.t(v)} : I18n.t(value)
-
else
-
attrs[key] = value
-
end
-
end
-
-
attrs['poll_options'] = raw_attrs.fetch('poll_options', []).map do |raw_option|
-
option = {}
-
raw_option.each_pair do |key, value|
-
if key.match /_i18n$/
-
option[key.gsub(/_i18n$/, '')] = I18n.t(value)
-
else
-
option[key] = value
-
end
-
end
-
option
-
end
-
-
PollTemplate.new attrs
-
end
-
end
-
-
def self.create(poll_template:, actor:)
-
actor.ability.authorize! :create, poll_template
-
-
poll_template.assign_attributes(author: actor)
-
-
return false unless poll_template.valid?
-
-
if poll_template.key
-
poll_template.group.hidden_poll_templates += Array(poll_template.key)
-
poll_template.key = nil
-
end
-
-
poll_template.save!
-
poll_template
-
end
-
-
def self.update(poll_template:, params:, actor:)
-
actor.ability.authorize! :update, poll_template
-
-
poll_template.assign_attributes_and_files(params.except(:group_id))
-
return false unless poll_template.valid?
-
poll_template.save!
-
-
poll_template
-
end
-
end
-
1
class ReactionService
-
1
def self.update(reaction:, params:, actor:)
-
5
actor.ability.authorize! :update, reaction
-
-
4
reaction.user = actor
-
4
reaction.assign_attributes(params.slice(:reaction))
-
-
4
return false unless reaction.valid?
-
4
reaction.save!
-
-
4
EventBus.broadcast 'reaction_create', reaction, actor
-
4
Events::ReactionCreated.publish!(reaction)
-
end
-
-
1
def self.destroy(reaction:, actor:)
-
2
actor.ability.authorize! :destroy, reaction
-
-
1
reaction.destroy
-
-
1
EventBus.broadcast 'reaction_destroy', reaction, actor
-
end
-
end
-
1
class ReceivedEmailService
-
1
def self.refresh_forward_email_rules
-
forward_email_rules = File.readlines(Rails.root.join("db/default_forward_email_rules.txt")).map(&:chomp).map do |handle|
-
{handle: handle, email: "#{handle}@#{ENV['REPLY_HOSTNAME']}"}
-
end
-
-
ForwardEmailRule.delete_all
-
ForwardEmailRule.insert_all(forward_email_rules, record_timestamps: false)
-
end
-
-
1
def self.route_all
-
ReceivedEmail.unreleased.each do |email|
-
route(email)
-
end
-
end
-
-
1
def self.route(email)
-
14
return nil unless email.route_address
-
14
return nil if email.released
-
14
return nil if email.sender_hostname.downcase == ENV['REPLY_HOSTNAME'].downcase
-
13
return nil if email.sender_hostname.downcase == ENV['SMTP_DOMAIN'].downcase
-
13
case email.route_path
-
when /d=.+&u=.+&k=.+/
-
# personal email-to-thread, eg. d=100&k=asdfghjkl&u=999@mail.loomio.com
-
3
if comment = CommentService.create(comment: Comment.new(comment_params(email)), actor: actor_from_email(email))
-
3
email.update_attribute(:released, true) if comment.persisted?
-
end
-
-
when /[^\s]+\+u=.+&k=.+/
-
# personal email-to-group, eg. enspiral+u=99&k=adsfghjl@mail.loomio.com
-
1
if discussion = DiscussionService.create(discussion: Discussion.new(discussion_params(email)), actor: actor_from_email(email))
-
1
email.update_attribute(:released, true) if discussion.persisted?
-
end
-
else
-
9
if forward_email_rule = ForwardEmailRule.find_by(handle: email.route_path)
-
1
ForwardMailer.forward_message(
-
from: "\"#{email.sender_name}\" <#{BaseMailer::NOTIFICATIONS_EMAIL_ADDRESS}>",
-
to: forward_email_rule.email,
-
reply_to: email.from,
-
subject: email.subject,
-
body_text: email.body_text,
-
body_html: email.body_html
-
).deliver_later
-
1
email.update(released: true)
-
1
return
-
end
-
-
8
if group = Group.find_by(handle: email.route_path)
-
7
if !address_is_blocked(email, group)
-
6
email.update(group_id: group.id)
-
-
6
if actor = actor_from_email_and_group(email, group)
-
4
if discussion = DiscussionService.create(discussion: Discussion.new(discussion_params(email)), actor: actor)
-
3
email.update(released: true) if discussion.persisted?
-
end
-
else
-
2
Events::UnknownSender.publish!(email)
-
end
-
end
-
end
-
end
-
rescue CanCan::AccessDenied, ActiveRecord::RecordNotFound
-
# TODO handle when user is not allowed to comment or create discussion
-
end
-
-
1
def self.extract_reply_body(text, author_name = nil)
-
11
return "" if text.strip.blank?
-
11
text.gsub!("\r\n", "\n")
-
-
# some emails match multiple split points, we run this until there are none
-
239
while regex = reply_split_points(author_name).find { |regex| regex.match? text } do
-
8
text = text.split(regex).first.strip
-
end
-
-
11
text.strip
-
end
-
-
1
def self.delete_old_emails
-
ReceivedEmail.where("created_at < ?", 60.days.ago).destroy_all
-
end
-
-
1
private
-
-
1
def self.reply_split_points(author_name = nil)
-
[
-
19
/^[[:space:]]*[-]+[[:space:]]*Original Message[[:space:]]*[-]+[[:space:]]*$/i,
-
/^[[:space:]]*--[[:space:]]*$/,
-
/^[[:space:]]*__[[:space:]]*$/,
-
/^[[:space:]]*\>?[[:space:]]*On.*\n?.*wrote:\n?$/,
-
/^[[:space:]]*\>?[[:space:]]*On.*\n?.*said:\n?$/,
-
/^On.*<\r?\n?.*>.*\r?\n?wrote:\r?\n?$/,
-
/On.*wrote:/,
-
19
(author_name ? /^[[:space:]]*#{author_name}[[:space:]]*$/ : nil), # signature that starts with author name
-
/#{EventMailer::REPLY_DELIMITER}/,
-
/\*?From:.*$/i,
-
/^[[:space:]]*\d{4}[-\/]\d{1,2}[-\/]\d{1,2}[[:space:]].*[[:space:]]<.*>?$/i,
-
/(_)*\n[[:space:]]*De :.*\n[[:space:]]*Envoyé :.*\n[[:space:]]*À :.*\n[[:space:]]*Objet :.*\n$/i, # French Outlook
-
/^[[:space:]]*\>?[[:space:]]*Le.*<\n?.*>.*\n?a[[:space:]]?\n?écrit :$/, # French
-
/^[[:space:]]*\>?[[:space:]]*El.*<\n?.*>.*\n?escribió:$/,
-
/^[[:space:]]*\>?[[:space:]]*El.*<\n?.*>.*\n?escribiÃ:$/,
-
/^[[:space:]]*\>?[[:space:]]*El.*<\n?.*>.*\n?escribió:$/
-
].compact
-
end
-
-
1
def self.parse_route_params(route_path)
-
12
params = {}.with_indifferent_access
-
-
12
if route_path.include?('+')
-
2
params['handle'] = route_path.split('+').first
-
end
-
-
12
route_path.split('+').last.split('&').each do |segment|
-
32
key_and_value = segment.split('=')
-
32
params[key_and_value[0]] = key_and_value[1]
-
end
-
-
12
params
-
end
-
-
1
def self.actor_from_email(email)
-
4
params = parse_route_params(email.route_path)
-
4
User.find_by!(id: params['u'], email_api_key: params['k'])
-
end
-
-
1
def self.address_is_blocked(email, group)
-
7
MemberEmailAlias.blocked.find_by(email: email.sender_email, group_id: group.id)
-
end
-
-
1
def self.actor_from_email_and_group(email, group)
-
6
if actor = (email.dkim_valid || email.spf_valid) && User.find_by(email: email.sender_email)
-
1
return actor if group.members.exists?(actor.id)
-
end
-
-
5
if email_alias = MemberEmailAlias.allowed.find_by(email: email.sender_email, group_id: group.id)
-
4
return nil if email_alias.require_dkim && !email.dkim_valid
-
3
return nil if email_alias.require_spf && !email.spf_valid
-
3
return email_alias.user if group.members.exists?(email_alias.user.id)
-
end
-
-
nil
-
end
-
-
1
def self.discussion_params(email)
-
5
params = parse_route_params(email.route_path)
-
5
group = Group.find_by!(handle: (params['handle'] || email.route_path))
-
{
-
5
group_id: group.id,
-
private: group.discussion_private_default,
-
title: email.subject,
-
body: email.full_body,
-
body_format: email.body_format,
-
files: email.attachments.map {|a| a.blob }
-
}.compact
-
end
-
-
1
def self.comment_params(email)
-
3
params = parse_route_params(email.route_path)
-
-
3
if params['c'].present?
-
1
parent_id = params['c']
-
1
parent_type = "Comment"
-
end
-
-
3
if params['pt'].present?
-
parent_type = {
-
1
'p' => 'Poll',
-
'c' => 'Comment',
-
's' => 'Stance',
-
'o' => 'Outcome'
-
}[params['pt']]
-
1
parent_id = params['pi']
-
end
-
-
{
-
3
discussion_id: params['d'].to_i,
-
parent_id: parent_id,
-
parent_type: parent_type,
-
body: email.reply_body,
-
body_format: 'md',
-
files: email.attachments.map {|a| a.blob }
-
}.compact
-
end
-
end
-
1
class RecordCache
-
1
attr_accessor :scope
-
1
attr_accessor :exclude_types
-
1
attr_accessor :user_ids
-
1
attr_accessor :current_user_id
-
-
1
def initialize
-
2308
@scope = {}.with_indifferent_access
-
2308
@user_ids = []
-
2308
@exclude_types = []
-
2308
@current_user_id = nil
-
end
-
-
1
def fetch(key_or_keys, id)
-
80713
(scope.dig(*Array(key_or_keys)) || {}).fetch(id) do
-
21936
if block_given?
-
21936
yield
-
else
-
raise "scope missing preloaded model: #{key_or_keys} #{id}"
-
end
-
end
-
end
-
-
1
def self.for_collection(collection, user_id, exclude_types = [])
-
2308
obj = self.new
-
2308
obj.exclude_types = exclude_types
-
2308
obj.current_user_id = user_id
-
2308
return obj unless item = collection.to_a.first
-
-
# puts "when #{item.class.to_s}"
-
2033
case item.class.to_s
-
when 'Discussion'
-
50
collection_ids = collection.map(&:id)
-
50
obj.add_discussions(collection)
-
50
obj.add_groups_subscriptions_memberships Group.with_attached_logo.with_attached_cover_photo.includes(:subscription).where(id: ids_and_parent_ids(Group, collection.map(&:group_id).compact))
-
50
obj.add_polls_options_stances_outcomes Poll.active.where(discussion_id: collection_ids)
-
-
when 'Reaction'
-
1
obj.user_ids.concat collection.map(&:user_id)
-
-
when 'Notification'
-
938
obj.add_events_complete Event.includes(:eventable).where(id: collection.map(&:event_id))
-
# obj.add_events_eventables Event.includes(:eventable).where(id: collection.map(&:event_id))
-
938
obj.user_ids.concat collection.map(&:user_id)
-
-
when 'Group'
-
11
obj.add_groups_subscriptions_memberships Group.with_attached_logo.with_attached_cover_photo.includes(:subscription).where(id: ids_and_parent_ids(Group, collection.map(&:id)))
-
-
when 'Membership'
-
15
obj.add_groups Group.with_attached_logo.with_attached_cover_photo.includes(:subscription).where(id: ids_and_parent_ids(Group, collection.map(&:group_id)))
-
15
obj.user_ids.concat collection.map(&:user_id).concat(collection.map(&:inviter_id).compact).compact.uniq
-
-
when 'Poll'
-
13
obj.add_groups Group.with_attached_logo.with_attached_cover_photo.includes(:subscription).where(id: ids_and_parent_ids(Group, collection.map(&:group_id)))
-
13
obj.add_discussions(Discussion.where(id: collection.map(&:discussion_id).uniq.compact))
-
13
obj.add_polls_options_stances_outcomes collection
-
-
when 'Outcome'
-
3
obj.add_polls Poll.where(id: collection.map(&:poll_id))
-
3
obj.user_ids.concat collection.map(&:author_id)
-
-
when 'Stance'
-
15
obj.add_stances(collection)
-
15
obj.add_polls_options_stances_outcomes Poll.kept.where(id: collection.map(&:poll_id))
-
-
when 'User'
-
# do nothing
-
-
when 'DiscussionReader'
-
6
obj.user_ids.concat collection.map(&:user_id)
-
-
when 'Comment'
-
2
obj.user_ids.concat collection.map(&:user_id)
-
-
when 'MembershipRequest'
-
4
obj.user_ids.concat collection.map(&:requestor_id).concat(collection.map(&:responder_id)).compact.uniq
-
-
when 'Document'
-
6
obj.user_ids.concat collection.map(&:author_id).compact
-
-
when 'SearchResult'
-
3
obj.user_ids.concat collection.map(&:author_id).compact
-
3
obj.add_polls_options_stances_outcomes Poll.kept.where(id: collection.map(&:poll_id))
-
-
else
-
938
obj.add_events_complete(collection) if item.is_a?(Event)
-
end
-
-
2033
obj.add_users User.with_attached_uploaded_avatar.where(id: obj.user_ids)
-
2033
obj.add_discussion_readers(DiscussionReader.where(discussion_id: obj.discussion_ids, user_id: user_id))
-
2033
obj.add_events Event.where(kind: 'new_discussion', eventable_id: obj.discussion_ids)
-
2033
obj.add_events Event.where(kind: 'discussion_forked', eventable_id: obj.discussion_ids)
-
2033
obj.add_events Event.where(kind: 'poll_created', eventable_id: obj.poll_ids)
-
2033
obj.add_tags_complete
-
2033
obj
-
end
-
-
1
def add_events_complete(collection)
-
1854
ids = {discussion: [], comment: [], group: [], poll: []}.with_indifferent_access
-
-
-
1854
Event.includes(:eventable).where(id: collection.map(&:id)).each do |e|
-
1865
ids[:discussion].push e.discussion_id if e.discussion_id
-
1865
next unless e.eventable
-
1865
ids[e.eventable_type.underscore] ||= []
-
1865
ids[e.eventable_type.underscore].push e.eventable_id
-
1865
if ['Stance', 'Outcome', 'PollOption'].include? e.eventable_type
-
160
ids[:poll].push e.eventable.poll_id
-
end
-
end
-
-
9458
ids.keys.each { |key| ids[key] = ids[key].uniq }
-
-
# Eventable specific stuff
-
# comments
-
# ids[:comment].concat self.class.all_parent_ids_for(Comment, ids[:comment]) if ids[:comment].any?
-
-
-
# find related group ids
-
1854
unless exclude_types.include?('group')
-
1845
ids[:group].concat Discussion.where(id: ids[:discussion]).pluck(:group_id)
-
1845
ids[:group].concat Poll.where(id: ids[:poll]).pluck(:group_id)
-
# ids[:group].concat all_parent_ids_for(Group, ids[:group])
-
end
-
-
1854
add_polls_options_stances_outcomes Poll.where(id: ids[:poll])
-
1854
add_discussions Discussion.where(id: ids[:discussion])
-
-
1854
add_events_eventables Event.includes(:eventable).where(id: self.class.ids_and_parent_ids(Event, collection.map(&:id)))
-
1854
add_groups_subscriptions_memberships Group.with_attached_logo.with_attached_cover_photo.includes(:subscription).where(id: ids[:group])
-
1854
add_comments Comment.where(id: ids[:comment])
-
# obj.add_reactions Reaction.where(id: ids[:reaction])
-
# obj.add_group_subscriptions Group.includes(:subscription).where(id: ids[:group])
-
# obj.add_events Event.where(kind: 'discussion_forked', eventable_id: @ids[:discussion])
-
# obj.add_events Event.where(kind: 'poll_created', eventable_id: @ids[:poll])
-
end
-
-
1
def self.ids_and_parent_ids(klass, ids)
-
1943
[ids, all_parent_ids_for(klass,ids)].flatten.uniq
-
end
-
-
1
def self.all_parent_ids_for(klass, ids)
-
6869
return [] if ids.empty?
-
4926
parent_ids = klass.where(id: ids).pluck(:parent_id)
-
4926
[parent_ids, all_parent_ids_for(klass, parent_ids)].flatten.uniq
-
end
-
-
# remember to join subscriptions for this call
-
1
def add_groups_subscriptions_memberships(collection)
-
1915
return [] if exclude_types.include?('group')
-
1906
group_ids = add_groups(collection)
-
1906
add_memberships(Membership.active.where(group_id: group_ids, user_id: current_user_id), group_ids)
-
1906
add_subscriptions(collection)
-
end
-
-
1
def add_groups(collection)
-
1934
return [] if exclude_types.include?('group')
-
1934
scope[:groups_by_id] ||= {}
-
1934
collection.map do |group|
-
1879
@user_ids.push group.creator_id
-
1879
scope[:groups_by_id][group.id] = group
-
1879
group.id
-
end
-
end
-
-
# this is a colleciton of groups joined to subscription.. crazy I know
-
1
def add_subscriptions(collection)
-
1906
return [] if exclude_types.include?('subscription')
-
1906
scope[:subscriptions_by_group_id] ||= {}
-
1906
collection.each do |group|
-
1850
scope[:subscriptions_by_group_id][group.id] = group.subscription if group.subscription
-
end
-
end
-
-
1
def add_memberships(collection, group_ids)
-
1906
return if exclude_types.include?('membership')
-
1906
scope[:memberships_by_group_id] ||= {}
-
1906
scope[:memberships_by_id] ||= {}
-
1906
collection.each do |m|
-
993
@user_ids.push m.user_id
-
993
@user_ids.push m.inviter_id if m.inviter_id
-
993
scope[:memberships_by_group_id][m.group_id] = m
-
993
scope[:memberships_by_id][m.id] = m
-
end
-
-
# is this buggy?
-
# our cache.fetch method benefits from knowing it is nil
-
# group_ids.each do |id|
-
# next if scope[:memberships_by_group_id].has_key?(id)
-
# scope[:memberships_by_group_id][id] = nil
-
# end
-
end
-
-
1
def add_polls_options_stances_outcomes(collection)
-
1935
return if exclude_types.include?('poll')
-
1935
collection_ids = collection.map(&:id)
-
1935
add_polls collection
-
1935
add_poll_options PollOption.where(poll_id: collection_ids)
-
1935
add_stances Stance.latest.where(poll_id: collection_ids, participant_id: current_user_id)
-
1935
add_outcomes Outcome.latest.where(poll_id: collection_ids)
-
end
-
-
1
def add_polls(collection)
-
1938
return if exclude_types.include?('poll')
-
1938
scope[:polls_by_discussion_id] ||= {}
-
1938
scope[:polls_by_id] ||= {}
-
1938
collection.each do |poll|
-
1224
@user_ids.push poll.author_id
-
1224
scope[:polls_by_id][poll.id] = poll
-
1224
scope[:polls_by_discussion_id][poll.discussion_id] ||= []
-
1224
scope[:polls_by_discussion_id][poll.discussion_id].push poll
-
end
-
end
-
-
1
def add_comments(collection)
-
1854
return [] if exclude_types.include?('comment')
-
1854
scope[:comments_by_id] ||= {}
-
1854
collection.each do |comment|
-
233
@user_ids.push comment.user_id
-
233
scope[:comments_by_id][comment.id] = comment
-
end
-
end
-
-
1
def add_tags_complete
-
2033
scope[:tags_by_type_and_id] ||= {}
-
-
2033
Tag.where(group_id: group_ids).each do |tag|
-
2127
scope[:tags_by_type_and_id]['Group'] ||= {}
-
2127
scope[:tags_by_type_and_id]['Group'][tag.group_id] ||= []
-
2127
scope[:tags_by_type_and_id]['Group'][tag.group_id].push tag
-
end
-
end
-
-
1
def add_outcomes(collection)
-
1935
return [] if exclude_types.include?('outcome')
-
1932
scope[:outcomes_by_id] ||= {}
-
1932
scope[:outcomes_by_poll_id] ||= {}
-
1932
collection.each do |outcome|
-
81
@user_ids.push outcome.author_id
-
81
scope[:outcomes_by_id][outcome.id] = outcome
-
81
scope[:outcomes_by_poll_id][outcome.poll_id] = outcome if outcome.latest
-
end
-
end
-
-
1
def add_reactions(collection)
-
return [] if ids.empty?
-
return [] if exclude_types.include?('reaction')
-
scope[:reactions_by_id] ||= {}
-
collection.each do |reaction|
-
@user_ids.push reaction.user_id
-
scope[:reactions_by_id][reaction.id] = reaction
-
end
-
end
-
-
1
def add_poll_options(collection)
-
1935
return [] if exclude_types.include?('poll_option')
-
1935
scope[:poll_options_by_id] ||= {}
-
1935
scope[:poll_options_by_poll_id] ||= {}
-
1935
collection.each do |poll_option|
-
4204
scope[:poll_options_by_id][poll_option.id] = poll_option
-
4204
scope[:poll_options_by_poll_id][poll_option.poll_id] ||= []
-
4204
scope[:poll_options_by_poll_id][poll_option.poll_id].push(poll_option)
-
end
-
end
-
-
1
def add_stances(collection)
-
1950
return [] if exclude_types.include?('stance')
-
1950
scope[:stances_by_id] ||= {}
-
1950
scope[:my_stances_by_poll_id] ||= {}
-
1950
collection.each do |stance|
-
915
@user_ids.push stance.participant_id
-
915
scope[:stances_by_id][stance.id] = stance
-
915
if stance.participant_id == current_user_id && stance.revoked_at.nil?
-
896
scope[:my_stances_by_poll_id][stance.poll_id] = stance
-
end
-
end
-
end
-
-
1
def add_events(collection)
-
7953
return [] if exclude_types.include?('event')
-
7915
scope[:events_by_id] ||= {}
-
7915
scope[:events_by_kind_and_eventable_id] ||= {}
-
-
7915
collection.each do |event|
-
5627
@user_ids.push event.user_id if event.user_id
-
5627
scope[:events_by_id][event.id] = event
-
5627
scope[:events_by_kind_and_eventable_id][event.kind] ||= {}
-
5627
scope[:events_by_kind_and_eventable_id][event.kind][event.eventable_id] = event
-
end
-
end
-
-
1
def add_events_eventables(collection)
-
1854
events = collection.includes(:eventable)
-
1854
add_events(events)
-
1854
add_eventables(events.map(&:eventable).compact)
-
end
-
-
1
def add_eventables(collection)
-
1854
collection.each do |eventable|
-
2897
@user_ids.push eventable.user_id if eventable.respond_to?(:user_id)
-
2897
scope["#{eventable.class.to_s.underscore.pluralize}_by_id"] ||= {}
-
2897
scope["#{eventable.class.to_s.underscore.pluralize}_by_id"][eventable.id] = eventable
-
end
-
end
-
-
1
def add_discussions(collection)
-
1917
return if exclude_types.include?('discussion')
-
1903
scope[:discussions_by_id] ||= {}
-
1903
collection.each do |discussion|
-
1461
@user_ids.push discussion.author_id
-
1461
scope[:discussions_by_id][discussion.id] = discussion
-
end
-
end
-
-
1
def add_discussion_readers(collection)
-
2033
return if exclude_types.include?('discussion_reader')
-
2033
scope[:discussion_readers_by_discussion_id] ||= {}
-
2033
collection.each do |dr|
-
95
scope[:discussion_readers_by_discussion_id][dr.discussion_id] = dr
-
end
-
end
-
-
1
def add_users(collection)
-
2033
return if exclude_types.include?('user')
-
2032
scope[:users_by_id] ||= {}
-
2032
collection.each do |user|
-
4134
scope[:users_by_id][user.id] = user
-
end
-
end
-
-
1
def group_ids
-
2033
scope.fetch(:groups_by_id, {}).keys
-
end
-
-
1
def discussion_ids
-
6099
scope.fetch(:discussions_by_id, {}).keys
-
end
-
-
1
def poll_ids
-
2033
scope.fetch(:polls_by_id, {}).keys
-
end
-
end
-
1
class RecordCloner
-
1
def initialize(recorded_at:)
-
3
@recorded_at = recorded_at
-
3
@cache = {}
-
end
-
-
1
def create_clone_group_for_public_demo(group, handle)
-
clone_group = new_clone_group(group)
-
clone_group.subscription = Subscription.new(plan: 'demo')
-
clone_group.handle = handle
-
clone_group.is_visible_to_public = true
-
clone_group.members_can_create_subgroups = false
-
clone_group.members_can_add_members = false
-
clone_group.members_can_add_guests = false
-
clone_group.members_can_announce = true
-
clone_group.discussion_privacy_options = 'public_only'
-
clone_group.membership_granted_upon = 'request'
-
clone_group.discussions.each {|d| d.private = false }
-
clone_group.polls.each {|p| p.specified_voters_only = false }
-
-
clone_group.save!
-
-
update_tag_colors(clone_group, group)
-
-
clone_group.polls.each do |poll|
-
poll.update_counts!
-
poll.stances.each {|s| s.update_option_scores!}
-
end
-
clone_group.discussions.each {|d| EventService.repair_thread(d.id) }
-
clone_group.reload
-
end
-
-
1
def update_tag_colors(clone_group, group)
-
1
group.tags.pluck(:name, :color).each do |pair|
-
2
Tag.where(group_id: clone_group.id, name: pair[0]).update_all(color: pair[1])
-
end
-
end
-
-
1
def create_clone_group_for_actor(group, actor)
-
# we don't really use this one except for testing
-
-
1
clone_group = new_clone_group(group)
-
1
clone_group.creator = actor
-
1
clone_group.subscription = Subscription.new(plan: 'demo', owner: actor)
-
1
clone_group.save!
-
-
1
update_tag_colors(clone_group, group)
-
1
store_source_record_ids(clone_group)
-
-
-
1
clone_group.polls.each do |poll|
-
1
poll.update_counts!
-
2
poll.stances.each {|s| s.update_option_scores!}
-
end
-
2
clone_group.discussions.each {|d| EventService.repair_thread(d.id) }
-
1
clone_group.add_member! actor
-
1
clone_group.reload
-
end
-
-
1
def clone_trial_content_into_group(group, actor)
-
source_group = Group.find_by(handle: 'trial-group-template')
-
-
group.discussions = source_group.discussions.kept.map {|d| new_clone_discussion_and_events(d) }
-
group.polls = source_group.polls.kept.map {|p| new_clone_poll(p) }
-
group.save!
-
-
update_tag_colors(group, source_group)
-
store_source_record_ids(group)
-
TranslationService.translate_group_content!(group, actor.locale)
-
-
-
group.polls.each do |poll|
-
poll.update_counts!
-
poll.stances.each {|s| s.update_option_scores!}
-
end
-
-
group.discussions.each {|d| EventService.repair_thread(d.id) }
-
group.reload
-
-
group.save!
-
-
group
-
end
-
-
1
def store_source_record_ids(clone_group)
-
1
source_ids = {}
-
1
@cache.each_pair do |key, value|
-
15
class_name, id = key.split('-')
-
15
source_ids["#{class_name}-#{value.id}"] = id.to_i
-
end
-
1
clone_group.info['source_record_ids'] = source_ids
-
1
clone_group.save!
-
end
-
-
-
1
def create_clone_group(group)
-
clone_group = new_clone_group(group)
-
clone_group.save!
-
-
update_tag_colors(clone_group, group)
-
-
store_source_record_ids(clone_group)
-
-
clone_group.polls.each do |poll|
-
poll.update_counts!
-
poll.stances.each {|s| s.update_option_scores!}
-
end
-
clone_group.discussions.each {|d| EventService.repair_thread(d.id) }
-
clone_group.reload
-
clone_group
-
end
-
-
1
def new_clone_group(group, clone_parent = nil)
-
copy_fields = %w[
-
1
name
-
description
-
description_format
-
members_can_add_members
-
members_can_edit_discussions
-
members_can_edit_comments
-
members_can_raise_motions
-
members_can_vote
-
members_can_start_discussions
-
members_can_create_subgroups
-
members_can_announce
-
new_threads_max_depth
-
new_threads_newest_first
-
admins_can_edit_user_content
-
members_can_add_guests
-
members_can_delete_comments
-
link_previews
-
created_at
-
updated_at
-
category
-
]
-
-
required_values = {
-
1
handle: nil,
-
is_visible_to_public: false,
-
is_visible_to_parent_members: false,
-
discussion_privacy_options: 'private_only',
-
membership_granted_upon: 'approval',
-
listed_in_explore: false
-
}
-
1
attachments = [:cover_photo, :logo, :files, :image_files]
-
-
1
clone_group = new_clone(group, copy_fields, required_values, attachments)
-
1
clone_group.parent = clone_parent
-
-
2
clone_group.memberships = group.memberships.map {|m| new_clone_membership(m) }
-
2
clone_group.discussions = group.discussions.kept.map {|d| new_clone_discussion_and_events(d) }
-
1
clone_group.subgroups = group.subgroups.published.map {|g| new_clone_group(g, clone_group) }
-
2
clone_group.polls = group.polls.kept.map {|p| new_clone_poll(p) }
-
-
1
clone_group
-
end
-
-
1
def new_clone_discussion(discussion)
-
copy_fields = %w[
-
2
author_id
-
title
-
discussion_template_id
-
discussion_template_key
-
description
-
description_format
-
pinned_at
-
max_depth
-
newest_first
-
content_locale
-
link_previews
-
created_at
-
updated_at
-
closed_at
-
last_activity_at
-
discarded_at
-
template
-
tags
-
]
-
-
2
required_values = {
-
private: true
-
}
-
-
2
attachments = [:files, :image_files]
-
2
new_clone(discussion, copy_fields, required_values, attachments)
-
end
-
-
1
def new_clone_discussion_and_events(discussion)
-
2
clone_discussion = new_clone_discussion(discussion)
-
2
created_event = new_clone_event(discussion.created_event)
-
2
created_event.eventable = clone_discussion
-
2
clone_discussion.events << created_event
-
2
drop_kinds = %w[poll_closed_by_user poll_expired poll_reopened]
-
18
clone_discussion.items = discussion.items.order(:sequence_id).select{|i| !drop_kinds.include?(i.kind) }.map { |event| new_clone_event_and_eventable(event) }
-
4
clone_discussion.polls = discussion.polls.map {|p| new_clone_poll(p) }
-
4
clone_discussion.comments = discussion.comments.order(:id).map { |c| new_clone_comment(c) }
-
2
clone_discussion
-
end
-
-
1
def new_clone_poll(poll)
-
copy_fields = %w[
-
6
author_id
-
closing_at
-
closed_at
-
created_at
-
updated_at
-
discarded_at
-
title
-
details
-
poll_type
-
process_name
-
process_subtitle
-
voter_can_add_options
-
anonymous
-
details_format
-
hide_results
-
discarded_by
-
specified_voters_only
-
notify_on_closing_soon
-
content_locale
-
link_previews
-
shuffle_options
-
limit_reason_length
-
meeting_duration
-
time_zone
-
dots_per_person
-
minimum_stance_choices
-
maximum_stance_choices
-
can_respond_maybe
-
min_score
-
max_score
-
template
-
agree_target
-
chart_type
-
default_duration_in_days
-
stance_reason_required
-
poll_option_name_format
-
reason_prompt
-
tags
-
poll_template_id
-
poll_template_key
-
]
-
6
attachments = [:files, :image_files]
-
-
6
clone_poll = new_clone(poll, copy_fields, {}, attachments)
-
25
clone_poll.poll_options = poll.poll_options.map {|poll_option| new_clone_poll_option(poll_option) }
-
12
clone_poll.stances = poll.stances.map {|stance| new_clone_stance(stance) }
-
12
clone_poll.outcomes = poll.outcomes.map {|outcome| new_clone_outcome(outcome) }
-
6
if !clone_poll.template
-
6
if poll.outcomes.empty?
-
clone_poll.closed_at = nil
-
clone_poll.closing_at = 3.days.from_now
-
else
-
6
clone_poll.closed_at = poll.outcomes.first.created_at
-
end
-
end
-
-
6
clone_poll
-
end
-
-
1
def new_clone_poll_option(poll_option)
-
copy_fields = %w[
-
19
name
-
icon
-
meaning
-
prompt
-
priority
-
score_counts
-
total_score
-
voter_scores
-
voter_count
-
]
-
19
clone_poll_option = new_clone(poll_option, copy_fields)
-
19
clone_poll_option.poll = existing_clone(poll_option.poll)
-
19
clone_poll_option
-
end
-
-
1
def new_clone_stance(stance)
-
copy_fields = %w[
-
8
accepted_at
-
admin
-
cast_at
-
content_locale
-
inviter_id
-
latest
-
link_previews
-
participant_id
-
reason
-
reason_format
-
revoked_at
-
created_at
-
updated_at
-
volume
-
]
-
8
attachments = [:files, :image_files]
-
8
clone_stance = new_clone(stance, copy_fields, {}, attachments)
-
16
clone_stance.stance_choices = stance.stance_choices.map {|sc| new_clone_stance_choice(sc) }
-
8
clone_stance.poll = existing_clone(stance.poll)
-
8
clone_stance
-
end
-
-
1
def new_clone_stance_choice(sc)
-
8
copy_fields = %w[ score ]
-
8
clone_sc = new_clone(sc, copy_fields)
-
8
clone_sc.poll_option = existing_clone(sc.poll_option)
-
8
clone_sc
-
end
-
-
1
def new_clone_outcome(outcome)
-
copy_fields = %w[
-
8
statement
-
latest
-
statement_format
-
author_id
-
review_on
-
content_locale
-
link_previews
-
created_at
-
updated_at
-
]
-
-
8
attachments = [:files, :image_files]
-
8
clone_outcome = new_clone(outcome, copy_fields, {}, attachments)
-
end
-
-
1
def new_clone_event(event)
-
copy_fields = %w[
-
10
user_id
-
kind
-
depth
-
sequence_id
-
position
-
position_key
-
child_count
-
pinned
-
descendant_count
-
custom_fields
-
created_at
-
]
-
10
new_clone(event, copy_fields)
-
end
-
-
1
def new_clone_event_and_eventable(event)
-
8
clone_event = new_clone_event(event)
-
-
8
case event.eventable_type
-
when 'Poll'
-
2
clone_event.eventable = new_clone_poll(event.eventable)
-
when 'Comment'
-
2
clone_event.eventable = new_clone_comment(event.eventable)
-
when 'Stance'
-
2
clone_event.eventable = new_clone_stance(event.eventable)
-
when 'Outcome'
-
2
clone_event.eventable = new_clone_outcome(event.eventable)
-
when 'Discussion'
-
clone_event.eventable = new_clone_discussion(event.eventable)
-
when nil
-
# nothing
-
else
-
raise "unrecognised eventable_type #{event.eventable_type}"
-
end
-
-
8
clone_event
-
end
-
-
1
def new_clone_membership(membership)
-
copy_fields = %w[
-
1
user_id
-
inviter_id
-
revoked_at
-
revoker_id
-
admin
-
volume
-
experiences
-
accepted_at
-
title
-
]
-
1
clone_membership = new_clone(membership, copy_fields)
-
1
clone_membership.group = existing_clone(membership.group)
-
1
clone_membership
-
end
-
-
1
def new_clone_comment(comment)
-
copy_fields = %w[
-
4
user_id
-
body
-
body_format
-
discarded_at
-
discarded_by
-
content_locale
-
link_previews
-
created_at
-
]
-
4
attachments = [:files, :image_files]
-
4
clone_comment = new_clone(comment, copy_fields, {}, attachments)
-
4
clone_comment.discussion = existing_clone(comment.discussion)
-
4
clone_comment.parent = existing_clone(comment.parent)
-
4
clone_comment
-
end
-
-
1
def new_clone_tag(tag)
-
clone_tag = new_clone(tag, %w[name color priority])
-
clone_tag.group = existing_clone(tag.group)
-
clone_tag
-
end
-
-
1
def new_clone(record, copy_fields = [], required_values = {}, attachments = [])
-
67
@cache["#{record.class}-#{record.id}"] ||= begin
-
39
clone = record.class.new
-
39
record_type = record.class.to_s.underscore.to_sym
-
-
39
clone.attributes = new_clone_attributes(record, copy_fields, required_values)
-
-
39
attachments.each do |name|
-
30
if clone.send(name).class == ActiveStorage::Attached::Many
-
28
clone.send(name).attach(record.send(name).blobs)
-
else
-
2
clone.send(name).attach record.send(name).blob
-
end
-
end
-
-
39
clone
-
end
-
end
-
-
1
def new_clone_attributes(record, copy_fields = [], required_values = {})
-
39
attrs = {}
-
39
copy_fields.each do |field|
-
482
value = record.send(field)
-
482
if value.nil?
-
103
attrs[field] = value
-
379
elsif field.ends_with?('_at')
-
45
attrs[field] = value.to_datetime + (DateTime.now - @recorded_at.to_datetime)
-
334
elsif field.ends_with?('_on')
-
attrs[field] = value.to_date + (Date.today - @recorded_at.to_date)
-
else
-
334
attrs[field] = value
-
end
-
end
-
-
39
required_values.each_pair do |key, value|
-
8
attrs[key] = value
-
end
-
-
39
attrs
-
end
-
-
1
def existing_clone(record)
-
44
@cache["#{record.class}-#{record.id}"]
-
end
-
end
-
class ReportService
-
def initialize(interval: 'month', group_ids: nil, start_at: 6.months.ago, end_at: 1.minute.ago)
-
@interval = interval
-
@group_ids = group_ids
-
@start_at = start_at
-
@end_at = end_at
-
@direct_threads = @group_ids.include?(0)
-
end
-
-
def intervals
-
vals = []
-
case @interval
-
when 'year'
-
next_val = @start_at.to_date.at_beginning_of_year
-
when 'month'
-
next_val = @start_at.to_date.at_beginning_of_month
-
when 'week'
-
next_val = @start_at.to_date.at_beginning_of_week
-
when 'day'
-
next_val = @start_at.to_date
-
else
-
raise "invalid interval value: #{@interval}"
-
end
-
-
while next_val < @end_at
-
vals.push(next_val)
-
next_val = (next_val + 1.send(@interval)).to_date
-
end
-
vals
-
end
-
-
# need to remember to sanitize group ids, any other args
-
def rows_to_hash(results, name_a = 'interval', name_b = 'count')
-
results.to_a.map {|row| [row[name_a], row[name_b]]}.to_h
-
end
-
-
def discussions_per_interval
-
query = <<~SQL
-
SELECT date_trunc('#{@interval}', discussions.created_at)::date AS interval, count(discussions.id) count
-
FROM discussions
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by interval
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute query
-
end
-
-
def comments_per_interval
-
query = <<~SQL
-
SELECT date_trunc('#{@interval}', comments.created_at)::date AS interval, count(comments.id) count
-
FROM comments
-
LEFT JOIN discussions ON comments.discussion_id = discussions.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND comments.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by interval
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute query
-
end
-
-
def polls_per_interval
-
query = <<~SQL
-
SELECT date_trunc('#{@interval}', polls.created_at)::date AS interval, count(polls.id) count
-
FROM polls
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by interval
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute query
-
end
-
-
def stances_per_interval
-
query = <<~SQL
-
SELECT date_trunc('#{@interval}', stances.created_at)::date AS interval, count(stances.id) count
-
FROM stances
-
LEFT JOIN polls ON stances.poll_id = polls.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND stances.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
AND stances.latest IS true
-
AND stances.cast_at IS NOT NULL
-
group by interval
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute query
-
end
-
-
def outcomes_per_interval
-
query = <<~SQL
-
SELECT date_trunc('#{@interval}', outcomes.created_at)::date AS interval, count(outcomes.id) count
-
FROM outcomes
-
LEFT JOIN polls ON outcomes.poll_id = polls.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND outcomes.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by interval
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute query
-
end
-
-
def discussions_count
-
query = <<~SQL
-
SELECT count(discussions.id) count
-
FROM discussions
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
SQL
-
ActiveRecord::Base.connection.execute(query).to_a.first['count']
-
end
-
-
def polls_count
-
query = <<~SQL
-
SELECT count(polls.id) count
-
FROM polls
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
SQL
-
ActiveRecord::Base.connection.execute(query).to_a.first['count']
-
end
-
-
def discussions_with_polls_count
-
query = <<~SQL
-
SELECT count(discussions.id) count
-
FROM discussions INNER JOIN polls ON discussions.id = polls.discussion_id
-
WHERE (discussions.group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR discussions.group_id IS NULL' : ''})
-
AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
SQL
-
ActiveRecord::Base.connection.execute(query).to_a.first['count']
-
end
-
-
def polls_with_outcomes_count
-
query = <<~SQL
-
SELECT count(polls.id) count
-
FROM polls INNER JOIN outcomes ON polls.id = outcomes.poll_id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
SQL
-
ActiveRecord::Base.connection.execute(query).to_a.first['count']
-
end
-
-
def discussion_ids
-
query = <<~SQL
-
SELECT discussions.id
-
FROM discussions
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
SQL
-
ActiveRecord::Base.connection.execute(query).map { |row| row['id'] }
-
end
-
-
def poll_ids
-
query = <<~SQL
-
SELECT polls.id
-
FROM polls
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
SQL
-
ActiveRecord::Base.connection.execute(query).map { |row| row['id'] }
-
end
-
-
def discussion_tag_counts
-
tag_counts = {}
-
Discussion.where(id: discussion_ids).each do |discussion|
-
discussion.tags.each {|tag| tag_counts[tag] = tag_counts.fetch(tag, 0) + 1 }
-
end
-
tag_counts
-
end
-
-
def poll_tag_counts
-
tag_counts = {}
-
Poll.where(id: poll_ids).each do |poll|
-
poll.tags.each {|tag| tag_counts[tag] = tag_counts.fetch(tag, 0) + 1 }
-
end
-
tag_counts
-
end
-
-
def tag_counts
-
total_counts = {}
-
discussion_tag_counts.each_pair {|tag, count| total_counts[tag] = total_counts.fetch(tag, 0) + count }
-
poll_tag_counts.each_pair {|tag, count| total_counts[tag] = total_counts.fetch(tag, 0) + count }
-
total_counts
-
end
-
-
def tag_names
-
(discussion_tag_counts.keys + poll_tag_counts.keys).uniq.sort
-
end
-
-
def discussions_per_user
-
query = <<~SQL
-
SELECT count(id) count, author_id user_id
-
FROM discussions
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by author_id
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'user_id', 'count'
-
end
-
-
def comments_per_user
-
query = <<~SQL
-
SELECT count(comments.id) count, user_id
-
FROM comments
-
JOIN discussions ON comments.discussion_id = discussions.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND comments.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by user_id
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'user_id', 'count'
-
end
-
-
def polls_per_user
-
query = <<~SQL
-
SELECT count(id) count, author_id user_id
-
FROM polls
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by author_id
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'user_id', 'count'
-
end
-
-
def outcomes_per_user
-
query = <<~SQL
-
SELECT count(outcomes.id) count, outcomes.author_id user_id
-
FROM outcomes
-
JOIN polls ON outcomes.poll_id = polls.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND outcomes.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by outcomes.author_id
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'user_id', 'count'
-
end
-
-
def stances_per_user
-
query = <<~SQL
-
SELECT count(stances.id) count, participant_id
-
FROM stances
-
JOIN polls ON stances.poll_id = polls.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND polls.anonymous = false
-
AND stances.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
AND stances.latest IS true
-
AND stances.cast_at IS NOT NULL
-
group by participant_id
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'participant_id', 'count'
-
end
-
-
def reactions_per_user
-
queries = []
-
data = {}
-
-
queries.push <<~SQL
-
SELECT count(reactions.id) count, reactions.user_id user_id
-
FROM reactions
-
JOIN comments ON reactions.reactable_id = comments.id AND reactions.reactable_type = 'Comment'
-
JOIN discussions ON comments.discussion_id = discussions.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by reactions.user_id
-
SQL
-
-
queries.push <<~SQL
-
SELECT count(reactions.id) count, reactions.user_id user_id
-
FROM reactions
-
JOIN discussions ON reactions.reactable_id = discussions.id AND reactions.reactable_type = 'Discussion'
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by reactions.user_id
-
SQL
-
-
queries.push <<~SQL
-
SELECT count(reactions.id) count, reactions.user_id user_id
-
FROM reactions
-
JOIN polls ON reactions.reactable_id = polls.id AND reactions.reactable_type = 'Poll'
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by reactions.user_id
-
SQL
-
-
queries.push <<~SQL
-
SELECT count(reactions.id) count, reactions.user_id user_id
-
FROM reactions
-
JOIN stances ON reactions.reactable_id = stances.id AND reactions.reactable_type = 'Stance'
-
JOIN polls ON stances.poll_id = polls.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by reactions.user_id
-
SQL
-
-
queries.push <<~SQL
-
SELECT count(reactions.id) count, reactions.user_id user_id
-
FROM reactions
-
JOIN outcomes ON reactions.reactable_id = outcomes.id AND reactions.reactable_type = 'Outcome'
-
JOIN polls ON outcomes.poll_id = polls.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by reactions.user_id
-
SQL
-
-
queries.each do |query|
-
rows_to_hash(ActiveRecord::Base.connection.execute(query), 'user_id', 'count').each_pair do |k, v|
-
data[k] = data.fetch(k, 0) + v
-
end
-
end
-
data
-
end
-
-
def users
-
if @direct_threads
-
User.all
-
else
-
user_ids = Membership.where(group_id: @group_ids).pluck(:user_id).uniq
-
User.where(id: user_ids)
-
end
-
end
-
-
def users_per_country
-
query = <<~SQL
-
SELECT count(DISTINCT users.id) count, country
-
FROM users
-
JOIN memberships ON memberships.user_id = users.id
-
WHERE email_verified = true
-
#{@direct_threads ? '' : "AND memberships.group_id IN (#{@group_ids.join(',')})"}
-
group by users.country
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
-
end
-
-
def discussions_per_country
-
query = <<~SQL
-
SELECT count(discussions.id) count, country
-
FROM discussions
-
JOIN users ON discussions.author_id = users.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND discussions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by users.country
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
-
end
-
-
def comments_per_country
-
query = <<~SQL
-
SELECT count(comments.id) count, country
-
FROM comments
-
JOIN discussions ON comments.discussion_id = discussions.id
-
JOIN users ON comments.user_id = users.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND comments.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by users.country
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
-
end
-
-
def polls_per_country
-
query = <<~SQL
-
SELECT count(polls.id) count, country
-
FROM polls
-
JOIN users ON polls.author_id = users.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND polls.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by users.country
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
-
end
-
-
def outcomes_per_country
-
query = <<~SQL
-
SELECT count(outcomes.id) count, country
-
FROM outcomes
-
JOIN polls ON polls.id = outcomes.poll_id
-
JOIN users ON outcomes.author_id = users.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND outcomes.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by users.country
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
-
end
-
-
def stances_per_country
-
query = <<~SQL
-
SELECT count(stances.id) count, country
-
FROM stances
-
JOIN polls ON stances.poll_id = polls.id
-
JOIN users ON stances.participant_id = users.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND polls.anonymous = false
-
AND stances.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
AND stances.latest IS true
-
AND stances.cast_at IS NOT NULL
-
group by country
-
SQL
-
rows_to_hash ActiveRecord::Base.connection.execute(query), 'country', 'count'
-
end
-
-
def reactions_per_country
-
queries = []
-
data = {}
-
-
queries.push <<~SQL
-
SELECT count(reactions.id) count, country
-
FROM reactions
-
JOIN comments ON reactions.reactable_id = comments.id AND reactions.reactable_type = 'Comment'
-
JOIN discussions ON comments.discussion_id = discussions.id
-
JOIN users ON reactions.user_id = users.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by country
-
SQL
-
-
queries.push <<~SQL
-
SELECT count(reactions.id) count, country
-
FROM reactions
-
JOIN discussions ON reactions.reactable_id = discussions.id AND reactions.reactable_type = 'Discussion'
-
JOIN users ON reactions.user_id = users.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by country
-
SQL
-
-
queries.push <<~SQL
-
SELECT count(reactions.id) count, country
-
FROM reactions
-
JOIN polls ON reactions.reactable_id = polls.id AND reactions.reactable_type = 'Poll'
-
JOIN users ON reactions.user_id = users.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by country
-
SQL
-
-
queries.push <<~SQL
-
SELECT count(reactions.id) count, country
-
FROM reactions
-
JOIN stances ON reactions.reactable_id = stances.id AND reactions.reactable_type = 'Stance'
-
JOIN polls ON stances.poll_id = polls.id
-
JOIN users ON reactions.user_id = users.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by country
-
SQL
-
-
queries.push <<~SQL
-
SELECT count(reactions.id) count, country
-
FROM reactions
-
JOIN outcomes ON reactions.reactable_id = outcomes.id AND reactions.reactable_type = 'Outcome'
-
JOIN polls ON outcomes.poll_id = polls.id
-
JOIN users ON reactions.user_id = users.id
-
WHERE (group_id IN (#{@group_ids.join(',')}) #{@direct_threads ? 'OR group_id IS NULL' : ''})
-
AND reactions.created_at BETWEEN '#{@start_at.iso8601}' AND '#{@end_at.iso8601}'
-
group by country
-
SQL
-
-
queries.each do |query|
-
rows_to_hash(ActiveRecord::Base.connection.execute(query), 'country', 'count').each_pair do |k, v|
-
data[k] = data.fetch(k, 0) + v
-
end
-
end
-
data
-
end
-
-
def countries
-
users.pluck(:country).uniq.map {|c| c.nil? ? 'Unknown' : c }.sort
-
end
-
end
-
1
module RetryOnError
-
1
def self.with_limit(limit)
-
474
limit.times do |i|
-
begin
-
478
return yield i
-
rescue => e
-
5
raise e if i + 1 == limit
-
end
-
end
-
end
-
end
-
1
class SearchService
-
1
def self.reindex_everything
-
[
-
Discussion.pg_search_insert_statement,
-
Comment.pg_search_insert_statement,
-
Poll.pg_search_insert_statement,
-
Stance.pg_search_insert_statement,
-
Outcome.pg_search_insert_statement
-
].each do |statement|
-
ActiveRecord::Base.connection.execute(statement)
-
end
-
end
-
-
1
def self.reindex_by_author_id(author_id)
-
11
PgSearch::Document.where(author_id: author_id).delete_all
-
-
[
-
11
Discussion.pg_search_insert_statement(author_id: author_id),
-
Comment.pg_search_insert_statement(author_id: author_id),
-
Poll.pg_search_insert_statement(author_id: author_id),
-
Stance.pg_search_insert_statement(author_id: author_id),
-
Outcome.pg_search_insert_statement(author_id: author_id),
-
].each do |statement|
-
55
ActiveRecord::Base.connection.execute(statement)
-
end
-
end
-
-
1
def self.reindex_by_discussion_id(discussion_id)
-
18
PgSearch::Document.where(discussion_id: discussion_id).delete_all
-
-
[
-
18
Discussion.pg_search_insert_statement(id: discussion_id),
-
Comment.pg_search_insert_statement(discussion_id: discussion_id),
-
Poll.pg_search_insert_statement(discussion_id: discussion_id),
-
Stance.pg_search_insert_statement(discussion_id: discussion_id),
-
Outcome.pg_search_insert_statement(discussion_id: discussion_id),
-
].each do |statement|
-
90
ActiveRecord::Base.connection.execute(statement)
-
end
-
end
-
-
1
def self.reindex_by_poll_id(poll_id)
-
27
PgSearch::Document.where(poll_id: poll_id).delete_all
-
-
[
-
27
Poll.pg_search_insert_statement(id: poll_id),
-
Stance.pg_search_insert_statement(poll_id: poll_id),
-
Outcome.pg_search_insert_statement(poll_id: poll_id),
-
].each do |statement|
-
81
ActiveRecord::Base.connection.execute(statement)
-
end
-
end
-
-
1
def self.reindex_by_comment_id(comment_id)
-
# Comment.find(comment_id).update_pg_search_document
-
PgSearch::Document.where(searchable_type: 'Comment', searchable_id: comment_id).delete_all
-
ActiveRecord::Base.connection.execute(Comment.pg_search_insert_statement(id: comment_id))
-
end
-
-
end
-
1
class SequenceService
-
1
def self.seq_present?(key, id)
-
1699
ActiveRecord::Base.connection.execute(
-
"SELECT 0 FROM partition_sequences where key = '#{key}' and id = #{id}"
-
).first.present?
-
end
-
-
1
def self.create_seq!(key, id, start)
-
1034
ActiveRecord::Base.connection.execute(
-
"INSERT INTO partition_sequences (key, id, counter) VALUES ('#{key}', #{id}, #{start}) ON CONFLICT DO NOTHING"
-
)
-
end
-
-
1
def self.next_seq!(key, id)
-
1699
ActiveRecord::Base.connection.execute(
-
"UPDATE partition_sequences SET counter = counter + 1 WHERE key = '#{key}' AND id = #{id} RETURNING (counter)"
-
)[0]["counter"]
-
end
-
-
1
def self.drop_seq!(key, id)
-
96
ActiveRecord::Base.connection.execute(
-
"DELETE FROM partition_sequences WHERE key = '#{key}' AND id = #{id}"
-
)
-
end
-
end
-
1
class StanceService
-
1
def self.create(stance:, actor:)
-
24
actor.ability.authorize!(:vote_in, stance.poll)
-
-
18
stance.participant = actor
-
18
stance.cast_at ||= Time.zone.now
-
18
stance.revoked_at = nil
-
18
stance.revoker_id = nil
-
18
stance.save!
-
13
stance.poll.update_counts!
-
-
13
event = Events::StanceCreated.publish!(stance)
-
13
event
-
end
-
-
1
def self.uncast(stance:, actor:)
-
2
actor.ability.authorize!(:uncast, stance)
-
-
1
new_stance = stance.build_replacement
-
1
Stance.transaction do
-
1
stance.update_columns(latest: false)
-
1
new_stance.save!
-
end
-
-
1
new_stance.poll.update_counts!
-
end
-
-
1
def self.update(stance: , actor: , params: )
-
52
actor.ability.authorize!(:update, stance)
-
52
is_update = !!stance.cast_at
-
-
52
new_stance = stance.build_replacement
-
52
new_stance.assign_attributes_and_files(params)
-
-
52
event = Event.where(eventable: stance, discussion_id: stance.poll.discussion_id).order('id desc').first
-
52
if is_update && stance.option_scores != new_stance.build_option_scores && event && event.child_count > 0
-
# they've changed their position and there were replies! create a new stance, so that discussion threads make sense
-
-
new_stance.cast_at = Time.zone.now
-
-
Stance.transaction do
-
stance.update_columns(latest: false)
-
new_stance.save!
-
end
-
-
new_stance.poll.update_counts!
-
MessageChannelService.publish_models([stance], group_id: stance.poll.group_id)
-
Events::StanceCreated.publish!(new_stance)
-
else
-
52
stance.stance_choices = []
-
52
stance.assign_attributes_and_files(params)
-
52
stance.cast_at ||= Time.zone.now
-
52
stance.revoked_at = nil
-
52
stance.revoker_id = nil
-
52
stance.save!
-
52
stance.poll.update_counts!
-
52
if is_update
-
Events::StanceUpdated.publish!(stance)
-
else
-
52
Events::StanceCreated.publish!(stance)
-
end
-
end
-
end
-
-
1
def self.redeem(stance:, actor:)
-
2
return if Stance.latest.where(participant_id: actor.id, poll_id: stance.poll_id).exists?
-
2
return unless Stance.redeemable_by(actor).where(id: stance.id).exists?
-
1
stance.update(participant: actor, accepted_at: Time.zone.now)
-
end
-
-
# def self.destroy(stance:, actor:)
-
# actor.ability.authorize! :destroy, stance
-
# stance.destroy
-
# EventBus.broadcast 'stance_destroy', stance, actor
-
# end
-
end
-
1
class TagService
-
1
def self.create(tag:, actor:)
-
1
actor.ability.authorize! :create, tag
-
-
1
return false unless tag.valid?
-
1
tag.save!
-
1
EventBus.broadcast 'tag_create', tag, actor
-
1
MessageChannelService.publish_models([tag], group_id: tag.group.id)
-
1
tag
-
end
-
-
1
def self.update(tag:, params:, actor:)
-
5
actor.ability.authorize! :update, tag
-
-
5
UpdateTagWorker.new.perform(tag.group_id, tag.name, params[:name].strip, params[:color])
-
5
tag.reload
-
-
5
MessageChannelService.publish_models([tag], group_id: tag.group.id)
-
5
EventBus.broadcast 'tag_update', tag, actor
-
5
tag
-
end
-
-
1
def self.destroy(tag:, actor:)
-
actor.ability.authorize! :destroy, tag
-
-
DestroyTagWorker.perform_async(tag.group_id, tag.name)
-
EventBus.broadcast 'tag_destroy', tag, actor
-
end
-
-
1
def self.apply_colors(group_id)
-
2531
group_ids = Group.find(group_id).parent_or_self.id_and_subgroup_ids
-
2531
Tag.where(group_id: group_id, color: nil).each do |tag|
-
286
if parent_tag = Tag.where(group_id: group_ids, name: tag.name).where.not(color: nil).first
-
13
tag.update_columns(color: parent_tag.color)
-
else
-
273
tag.update_columns(color: Tag::COLORS.sample)
-
end
-
end
-
end
-
-
1
def self.update_group_and_org_tags(group_id)
-
5172
update_group_tags(group_id)
-
5172
update_org_tagging_counts(Group.find(group_id).parent_or_self.id)
-
end
-
-
1
def self.update_group_tags(group_id)
-
5186
return unless group = Group.find_by(id: group_id)
-
-
5186
names = (group.discussions.kept.select(:tags).pluck(:tags).flatten +
-
group.polls.kept.select(:tags).pluck(:tags).flatten)
-
-
5186
return if names.empty?
-
-
1270
counts = {}
-
-
1270
names.map(&:downcase).each do |dname|
-
2241
counts[dname] ||= 0
-
2241
counts[dname] += 1
-
end
-
-
1270
group.tags.where.not(name: counts.keys).update_all(taggings_count: 0)
-
-
1270
present = Tag.where(group_id: group_id, name: counts.keys).pluck(:name).map(&:downcase)
-
1270
missing = counts.keys - present
-
-
1270
Tag.where(group_id: group_id, name: present).each do |tag|
-
1871
tag.update_column(:taggings_count, counts[tag.name.downcase])
-
end
-
-
1270
missing.each do |dname|
-
285
Tag.insert({group_id: group_id,
-
427
name: names.find {|name| name.downcase == dname},
-
taggings_count: counts[dname]})
-
end
-
-
1270
apply_colors(group_id)
-
end
-
-
1
def self.update_org_tagging_counts(group_id)
-
5177
return unless group = Group.find_by(id: group_id)
-
-
5177
group_ids = group.id_and_subgroup_ids
-
-
5177
names = Tag.where(group_id: group_ids).pluck(:name).uniq
-
-
5177
return if names.empty?
-
-
1261
counts = {}
-
-
1261
Tag.where(group_id: group_ids).pluck(:name).map(&:downcase).uniq.map do |dname|
-
2141
counts[dname] = Tag.where(group_id: group_ids, name: dname).sum(:taggings_count)
-
end
-
-
1261
group.tags.where.not(name: counts.keys).update_all(org_taggings_count: 0)
-
-
1261
present = Tag.where(group_id: group_id, name: names).pluck(:name).map(&:downcase)
-
1261
missing = counts.keys - present
-
-
1261
Tag.where(group_id: group_id, name: present).each do |tag|
-
2140
tag.update_column(:org_taggings_count, counts[tag.name.downcase])
-
end
-
-
1261
missing.each do |dname|
-
1
Tag.insert({group_id: group_id,
-
2
name: names.find {|name| name.downcase == dname},
-
org_taggings_count: counts[dname]})
-
end
-
-
1261
apply_colors(group_id)
-
end
-
end
-
1
class TaskService
-
1
def self.send_task_reminders(time = Time.now.utc.at_beginning_of_hour)
-
1
Task.not_done.where(remind_at: time).each do |task|
-
1
task.users.each do |user|
-
1
TaskMailer.task_due_reminder(user, task).deliver_later
-
end
-
end
-
end
-
-
1
def self.update_done(task, actor, done)
-
-
5
task.done = done
-
5
task.done_at = (done && Time.now) || nil
-
5
task.doer = (done && actor) || nil
-
-
5
record = task.record
-
-
5
doc = Nokogiri::HTML::DocumentFragment.parse(record.body)
-
5
doc.css("li[data-uid='#{task.uid}']").each do |li|
-
5
li['data-checked'] = done ? 'true' : 'false'
-
end
-
-
5
record.body = doc.to_html
-
-
5
record.save!
-
5
task.save!
-
-
5
if record.group_id
-
5
MessageChannelService.publish_models([record], group_id: record.group.id)
-
end
-
-
5
if record.respond_to?(:guests)
-
5
record.guests.find_each do |user|
-
MessageChannelService.publish_models([record], user_id: user.id)
-
end
-
end
-
end
-
-
1
def self.rewrite_uids(text)
-
76386
node = Nokogiri::HTML::fragment(text)
-
76386
uids = []
-
-
-
76386
node.search('li[data-type="taskItem"]').each do |el|
-
10
if uids.include?(el['data-uid'].to_i)
-
el['data-uid'] = (rand() * 100000000).to_i
-
end
-
10
uids.push el['data-uid'].to_i
-
end
-
-
76386
node.to_html
-
end
-
-
1
def self.parse_and_update(model, field)
-
76385
update_model(model, parse_tasks(model[field], model.author))
-
end
-
-
1
def self.parse_tasks(rich_text, author)
-
76394
Nokogiri::HTML::fragment(rich_text).search('li[data-type="taskItem"]').map do |el|
-
18
identifiers = Nokogiri::HTML::fragment(el).
-
search("span[data-mention-id]").map do |el|
-
14
el['data-mention-id']
-
end
-
32
usernames = identifiers.filter { |id_or_username| id_or_username.to_i.to_s != id_or_username }
-
32
user_ids = identifiers.filter { |id_or_username| id_or_username.to_i.to_s == id_or_username }
-
-
18
remind = (el['data-remind'].present? ? el['data-remind'].to_i : nil)
-
18
due_on = el['data-due-on'].to_s.to_date
-
18
remind_at = (due_on && remind) ? ("#{el['data-due-on']} 06:00".in_time_zone(author.time_zone) - remind.day) : nil
-
{
-
18
uid: el['data-uid'].to_i,
-
name: el.text,
-
user_ids: user_ids,
-
usernames: usernames,
-
due_on: el['data-due-on'].to_s.to_date,
-
remind: remind,
-
remind_at: remind_at,
-
done: el['data-checked'] == 'true',
-
18
author_id: (el['data-author-id'] && el['data-author-id'].to_i) || author.id
-
}
-
end
-
end
-
-
1
def self.update_model(model, tasks_data)
-
76408
uids = tasks_data.map {|t| t[:uid] }
-
76392
existing_uids = model.tasks.pluck(:uid)
-
76392
new_uids = uids - existing_uids
-
76392
removed_uids = existing_uids - uids
-
-
# delete tasks which are not mentioned by uid
-
# TODO maybe notify people if a task is deleted. or mark it as discarded
-
76392
model.tasks.where(uid: removed_uids).destroy_all
-
-
# update existing tasks
-
76392
model.tasks.where(uid: existing_uids).each do |task|
-
12
data = tasks_data.find { |t| t[:uid] == task.uid }
-
-
6
mentioned_users = model.members.where('users.id in (:ids) or users.username in (:names)',
-
ids: data[:user_ids],
-
names: data[:usernames])
-
6
new_users = mentioned_users.where('users.id not in (?)', task.users.pluck(:id))
-
6
removed_users = task.users.where('users.id not in (?)', mentioned_users.pluck(:id))
-
-
6
task.update!(name: data[:name],
-
due_on: data[:due_on],
-
users: mentioned_users,
-
done: data[:done],
-
remind: data[:remind],
-
remind_at: data[:remind_at],
-
6
done_at: (!task.done && data[:done]) ? Time.now : task.done_at,
-
author: model.members.find_by('users.id': data[:author_id]) || model.author)
-
end
-
-
# create tasks which dont yet exist
-
76408
tasks_data.filter{|t| new_uids.include?(t[:uid]) }.each do |data|
-
10
users = model.members.where('users.id in (:ids) or users.username in (:names)',
-
ids: data[:user_ids],
-
names: data[:usernames])
-
10
model.tasks.create(
-
uid: data[:uid],
-
name: data[:name],
-
due_on: data[:due_on],
-
remind: data[:remind],
-
remind_at: data[:remind_at],
-
users: users,
-
done: data[:done],
-
10
done_at: (data[:done] ? Time.now : nil),
-
author: model.members.find_by('users.id': data[:author_id]) || model.author
-
)
-
end
-
end
-
end
-
1
module ThrottleService
-
1
class LimitReached < StandardError
-
end
-
-
1
def self.reset!(per)
-
2
CACHE_REDIS_POOL.with do |client|
-
3
client.scan_each(match: "THROTTLE-#{per.upcase}*") { |key| client.del(key) }
-
end
-
end
-
-
1
def self.can?(key: 'default-key', id: 1, max: 100 , inc: 1, per: 'hour')
-
1236
raise "Throttle per is not hour or day: #{per}" unless ['hour', 'day'].include? per.to_s
-
1236
k = "THROTTLE-#{per.upcase}-#{key}-#{id}"
-
1236
Redis::Counter.new(k).increment(inc)
-
1236
Redis::Counter.new(k).value <= ENV.fetch('THROTTLE_MAX_'+key, max)
-
end
-
-
1
def self.limit!(key: 'default-key', id: 1, max: 100 , inc: 1, per: 'hour')
-
1216
if can?(key: key, id: id, max: max, inc: inc, per: per)
-
1215
return true
-
else
-
1
raise ThrottleService::LimitReached.new "Throttled! #{key}-#{id}"
-
end
-
end
-
end
-
1
class TranscriptionService
-
1
def self.available?
-
21
ENV['OPENAI_API_KEY'].present?
-
end
-
-
1
def self.transcribe(file)
-
client = OpenAI::Client.new(access_token: ENV.fetch('OPENAI_API_KEY'))
-
client.audio.transcribe(
-
parameters: {
-
model: "whisper-1",
-
file: file,
-
response_format: :verbose_json,
-
}
-
)
-
end
-
end
-
1
require "google/cloud/translate"
-
-
1
class TranslationService
-
1
extend LocalesHelper
-
-
1
GOOGLE_LOCALES = %w[af sq am ar hy as ay az bm eu be bn bho bs bg ca ceb zh-CN zh zh-TW co hr cs da dv doi nl en eo et ee fil fi fr fy gl ka de el gn gu ht ha haw he iw hi hmn hu is ig ilo id ga it ja jv jw kn kk km rw gom ko kri ku ckb ky lo la lv ln lt lg lb mk mai mg ms ml mt mi mr mni-Mtei lus mn my ne no ny or om ps fa pl pt pa qu ro ru sm sa gd nso sr st sn sd si sk sl so es su sw sv tl tg ta tt te th ti ts tr tk ak uk ur ug uz vi cy xh yi yo zu]
-
-
1
def self.locale_for_google(locale)
-
locale = locale.downcase.gsub("_", "-")
-
return locale if GOOGLE_LOCALES.include?(locale)
-
locale.split("-")[0]
-
end
-
-
1
def self.create(model:, to:)
-
locale = locale_for_google(to)
-
translation = model.translations.find_by(language: locale) ||
-
Translation.new(translatable: model, language: locale, fields: {})
-
-
if translation.new_record? || ((translation.updated_at || translation.created_at) < (model.updated_at || model.created_at || 5.years.ago))
-
service = Google::Cloud::Translate.translation_v2_service
-
-
model.class.translatable_fields.each do |field|
-
next if model.send(field).blank?
-
translation.fields[field.to_s] = service.translate(model.send(field), to: locale)
-
end
-
-
translation.save!
-
end
-
-
translation
-
end
-
-
1
def self.available?
-
57501
ENV['TRANSLATE_CREDENTIALS'].present?
-
end
-
-
1
def self.translate_group_content!(group, locale, cache_only = false)
-
return if locale == 'en'
-
-
translate_group_record(group, group, locale, cache_only)
-
-
group.discussions.each do |discussion|
-
translate_group_record(group, discussion, locale, cache_only)
-
end
-
-
group.polls.each do |poll|
-
translate_group_record(group, poll, locale, cache_only)
-
-
poll.outcomes.each do |outcome|
-
translate_group_record(group, outcome, locale, cache_only)
-
end
-
-
poll.poll_options.each do |poll_option|
-
if poll.poll_option_name_format != 'plain'
-
translate_group_record(group, poll_option, locale, cache_only, ignore: 'name')
-
else
-
translate_group_record(group, poll_option, locale, cache_only)
-
end
-
end
-
-
poll.stances.each do |stance|
-
translate_group_record(group, stance, locale, cache_only)
-
end
-
end
-
-
group.comments.each do |comment|
-
translate_group_record(group, comment, locale, cache_only)
-
end
-
-
group.tags.each do |tag|
-
translate_group_record(group, tag, locale, cache_only)
-
end
-
end
-
-
1
def self.translate_group_record(group, record, locale, cache_only = false, ignore: [])
-
translate_record = if source_record_id = group.info.dig('source_record_ids', "#{record.class.to_s}-#{record.id}")
-
record.class.find(source_record_id)
-
else
-
record
-
end
-
-
translation = TranslationService.create(model: translate_record, to: locale)
-
-
return if cache_only
-
-
translation.fields.each do |pair|
-
next if ignore.include?(pair[0])
-
record.update_attribute(pair[0], pair[1])
-
end
-
-
record.update_content_locale if record.has_attribute?(:content_locale)
-
end
-
end
-
1
class UserService
-
1
class EmailTakenError < StandardError
-
end
-
-
1
def self.create(params:)
-
10
if User.where(email_verified: true, email: params[:email]).exists?
-
1
raise UserService::EmailTakenError.new(email: params[:email])
-
end
-
-
9
user = User.where(email_verified: false, email: params[:email]).first_or_create
-
9
user.attributes = params.slice(:name, :email, :recaptcha, :legal_accepted, :email_newsletter)
-
9
user.require_valid_signup = true
-
9
user.require_recaptcha = true
-
9
user.save
-
-
9
user
-
rescue ActiveRecord::RecordNotUnique
-
retry
-
end
-
-
1
def self.verify(user: )
-
129
return user if user.email_verified?
-
-
8
user = User.verified.find_by(email: user.email) || user.tap{ |u| u.update(email_verified: true) }
-
-
4
if user.email_newsletter?
-
GenericWorker.perform_async('NewsletterService', 'subscribe', user.name, user.email)
-
end
-
-
4
user
-
end
-
-
1
def self.deactivate(user:, actor:)
-
2
actor.ability.authorize! :deactivate, user
-
2
DeactivateUserWorker.perform_async(user.id, actor.id)
-
end
-
-
1
def self.redact(user:, actor:)
-
4
actor.ability.authorize! :redact, user
-
4
RedactUserWorker.perform_async(user.id, actor.id)
-
end
-
-
1
def self.reactivate(user_id)
-
1
user = User.find(user_id)
-
1
deactivated_at = user.deactivated_at
-
1
Membership.where(user_id: user.id, revoked_at: deactivated_at).update_all(revoked_at: nil, revoker_id: nil)
-
1
group_ids = Membership.where(user_id: user.id).pluck(:group_id)
-
1
Group.where(id: group_ids).map(&:update_memberships_count)
-
1
user.update(deactivated_at: nil)
-
1
GenericWorker.perform_async('SearchService', 'reindex_by_author_id', user.id)
-
end
-
-
1
def self.set_volume(user:, actor:, params:)
-
2
actor.ability.authorize! :update, user
-
2
user.update!(default_membership_volume: params[:volume])
-
2
if params[:apply_to_all]
-
1
user.memberships.update_all(volume: Membership.volumes[params[:volume]])
-
1
user.discussion_readers.update_all(volume: Membership.volumes[params[:volume]])
-
1
user.stances.update_all(volume: Membership.volumes[params[:volume]])
-
end
-
2
EventBus.broadcast('user_set_volume', user, actor, params)
-
end
-
-
1
def self.update(user:, actor:, params:)
-
3
actor.ability.authorize! :update, user
-
3
user.assign_attributes_and_files(params)
-
3
return false unless user.valid?
-
3
user.save!
-
3
EventBus.broadcast('user_update', user, actor, params)
-
3
GenericWorker.perform_async('SearchService', 'reindex_by_author_id', user.id) if user.name_previously_changed?
-
end
-
-
1
def self.save_experience(user:, actor:, params:)
-
2
actor.ability.authorize! :update, user
-
1
name = params[:experience]
-
1
value = if params.has_key?(:remove_experience)
-
nil
-
else
-
1
params.fetch(:value, true)
-
end
-
1
user.experiences[name] = value
-
1
user.save!
-
1
EventBus.broadcast('user_save_experience', user, actor, params)
-
end
-
end
-
class EmailValidator < ActiveModel::EachValidator
-
def validate_each(record, attribute, value)
-
unless value =~ Devise.email_regexp
-
record.errors.add(attribute, "Not a valid email")
-
end
-
end
-
end
-
class AcceptMembershipWorker
-
include Sidekiq::Worker
-
-
def perform(membership_id, user_id)
-
return unless membership = Membership.pending.find_by(id: membership_id)
-
user = User.find(user_id)
-
MembershipService.redeem(membership: membership, actor: user, notify: false)
-
end
-
end
-
class AddGroupIdToDocumentsWorker
-
include Sidekiq::Worker
-
-
def perform
-
Document.where(group_id: nil).find_each do |document|
-
document.update_column(:group_id, document.model.group_id) if document.model && document.model.respond_to?(:group_id)
-
end
-
end
-
end
-
class AddHeadingIdsWorker
-
include Sidekiq::Worker
-
-
def perform()
-
{
-
Discussion => 'description',
-
Comment => 'body',
-
Poll => 'details',
-
Outcome => 'statement',
-
Stance => 'reason',
-
Group => 'description'
-
}.each_pair do |model, field|
-
rel = model.where("#{field}_format": 'html').where("#{field} is not null and #{field} != ''")
-
puts "Updating #{rel.count} #{model.to_s.pluralize}"
-
rel.find_each do |r|
-
model.where(id: r.id).update_all(field => HasRichText::add_heading_ids(r[field]))
-
end
-
end
-
end
-
end
-
class AnnounceDiscussionWorker
-
include Sidekiq::Worker
-
-
def perform(discussion_id, actor_id, params)
-
DiscussionService.invite(
-
discussion: Discussion.find(discussion_id),
-
actor: User.find(actor_id),
-
params: params.with_indifferent_access
-
)
-
end
-
end
-
class AppendTranscriptWorker
-
include Sidekiq::Worker
-
-
def perform(blob_id)
-
blob = ActiveStorage::Blob.find(blob_id)
-
-
text = blob.metadata['text']
-
blob.attachments.each do |attachment|
-
record = attachment.record
-
record.body = record.body + "<p>#{text}</p>"
-
record.save!
-
MessageChannelService.publish_models(Array(record), group_id: record.group_id)
-
end
-
end
-
end
-
class AttachDocumentWorker
-
include Sidekiq::Worker
-
-
def perform(document_id)
-
d = Document.find(document_id)
-
return if d.file.attached?
-
s3 = ActiveStorage::Blob.service
-
path = URI(d.url).path.gsub("/attachments", "attachments")
-
obj = s3.bucket.object(path)
-
params = {filename: obj.key, content_type: obj.content_type, byte_size: obj.size, checksum: obj.etag.gsub('"',"") }
-
blob = ActiveStorage::Blob.create_before_direct_upload!(**params)
-
blob.key = obj.key
-
d.file.attach(blob)
-
end
-
end
-
1
class CloseExpiredPollWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(poll_id)
-
15
poll = Poll.find(poll_id)
-
15
return if poll.closed_at
-
15
PollService.do_closing_work(poll: poll)
-
15
Events::PollExpired.publish!(poll)
-
end
-
end
-
class ConvertDiscussionTemplatesWorker
-
include Sidekiq::Worker
-
-
def perform
-
Discussion.where(template: true).each do |discussion|
-
template = DiscussionTemplate.new(discussion.slice(
-
:group_id,
-
:author_id,
-
:title,
-
:description,
-
:description_format,
-
:tags,
-
:max_depth,
-
:newest_first,
-
:content_locale,
-
:link_previews,
-
:discarded_at,
-
:discarded_by,
-
:created_at,
-
:updated_at,
-
:attachments))
-
-
template.process_name = discussion.title
-
template.source_discussion_id = discussion.id
-
-
template.save!
-
-
discussion.files.attachments.update_all(
-
record_type: 'DiscussionTemplate',
-
record_id: template.id
-
)
-
-
discussion.image_files.attachments.update_all(
-
record_type: 'DiscussionTemplate',
-
record_id: template.id
-
)
-
-
discussion.discard! if discussion.kept?
-
end
-
end
-
end
-
class ConvertPollStancesInDiscussionWorker
-
include Sidekiq::Worker
-
sidekiq_options queue: :low, retry: false
-
-
def perform(poll_id)
-
poll = Poll.find(poll_id)
-
return if !poll.discussion_id
-
return if poll.stances_in_discussion
-
poll.update_attribute(:stances_in_discussion, true)
-
stance_ids = poll.stances.latest.reject(&:body_is_blank?).map(&:id)
-
Stance.where(id: stance_ids).each do |stance|
-
stance.create_missing_created_event! if stance.created_event.nil?
-
end
-
Event.where(kind: 'stance_created', eventable_id: stance_ids, discussion_id: nil).update_all(discussion_id: poll.discussion_id)
-
EventService.repair_thread(poll.discussion_id)
-
end
-
end
-
1
class DeactivateUserWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(user_id, actor_id)
-
3
user = User.find(user_id)
-
3
deactivated_at = DateTime.now
-
3
group_ids = Membership.active.where(user_id: user_id).pluck(:group_id)
-
-
3
User.transaction do
-
-
3
MembershipService.revoke_by_id(group_ids, user_id, actor_id, deactivated_at)
-
-
3
user.update(deactivated_at: deactivated_at, deactivator_id: actor_id)
-
3
MembershipRequest.where(requestor_id: user_id, responded_at: nil).destroy_all
-
end
-
-
3
SearchService.reindex_by_author_id(user.id)
-
end
-
end
-
class DestroyDiscussionWorker
-
include Sidekiq::Worker
-
-
def perform(discussion_id)
-
Discussion.discarded.find(discussion_id).destroy!
-
end
-
end
-
1
class DestroyGroupWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(group_id)
-
1
Group.archived.find_by(id: group_id).try(:destroy!)
-
end
-
end
-
1
class DestroyRecordWorker
-
1
include Sidekiq::Worker
-
1
def perform(class_name, record_id)
-
1
class_name.constantize.find(record_id).destroy!
-
end
-
end
-
class DestroyTagWorker
-
include Sidekiq::Worker
-
-
def perform(group_id, name)
-
group = Group.find(group_id)
-
group_ids = group.id_and_subgroup_ids
-
-
Tag.transaction do
-
Tag.where(group_id: group_ids, name: name).delete_all
-
-
Discussion.where(group_id: group_ids).where.contains(tags: [name]).find_each do |d|
-
d.update_column(:tags, d.tags - Array(name))
-
end
-
-
Poll.where(group_id: group_ids).where.contains(tags: [name]).find_each do |p|
-
p.update_column(:tags, p.tags - Array(name))
-
end
-
end
-
-
TagService.update_org_tagging_counts(group.parent_or_self.id)
-
end
-
end
-
1
class DestroyUserWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(user_id)
-
5
ActiveRecord::Base.transaction do
-
-
# invited_user_ids = []
-
# invited_user_ids.concat DiscussionReader.where(inviter_id: user_id).pluck(:user_id)
-
# invited_user_ids.concat Membership.where(inviter_id: user_id).pluck(:user_id)
-
# invited_user_ids.concat Stance.where(inviter_id: user_id).pluck(:participant_id)
-
# invited_user_ids = User.where(email_verified: false).where(id: invited_user_ids).pluck(:id)
-
#
-
# event_ids = Event.where(user_id: user_id).pluck(:id)
-
# Notification.where(event_id: event_ids).delete_all
-
# DiscussionReader.where(inviter_id: user_id).delete_all
-
# Membership.where(inviter_id: user_id).delete_all
-
# User.where(id: invited_user_ids).delete_all
-
# Event.where(user_id: user_id).delete_all
-
#
-
#
-
# User.find(user_id).destroy!
-
-
5
User.find(user_id).destroy!
-
end
-
end
-
end
-
class DownloadAttachmentWorker
-
include Sidekiq::Worker
-
-
def perform(record, new_id)
-
GroupExportService.download_attachment(record, new_id)
-
end
-
end
-
class FixStancesMissingFromThreadsWorker
-
include Sidekiq::Worker
-
def perform
-
stance_ids = Event.where("discussion_id is not null").where(eventable_type: 'Stance').pluck(:eventable_id)
-
poll_ids = Stance.where(id: stance_ids).pluck(:poll_id).uniq
-
Stance.joins(:poll).
-
where("polls.discussion_id is not null").
-
where("reason is not null").
-
where("stances.poll_id NOT IN (?)", poll_ids).find_each do |s|
-
s.create_missing_created_event! if s.add_to_discussion?
-
end
-
end
-
end
-
1
class GenericWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(class_name, method_name, arg1 = nil, arg2 = nil, arg3 = nil, arg4 = nil, arg5 = nil)
-
12211
class_name.constantize.send(method_name, *([arg1, arg2, arg3, arg4, arg5].compact))
-
end
-
end
-
class GeoLocationWorker
-
include Sidekiq::Worker
-
-
def perform
-
db_filename = Rails.root.join('public', 'GeoLite2-Country.mmdb').to_s
-
-
unless File.exist? db_filename
-
# from https://github.com/P3TERX/GeoLite.mmdb
-
download = URI.parse("https://git.io/GeoLite2-Country.mmdb").open
-
IO.copy_stream(download, db_filename)
-
puts "downloaded maxmind db"
-
end
-
-
db = MaxMindDB.new(db_filename)
-
User.where(country: nil).where("current_sign_in_ip is not null").find_each do |user|
-
record = db.lookup(user.current_sign_in_ip.to_s)
-
next unless record.found?
-
user.update_columns(country: record.country.name)
-
end
-
end
-
end
-
class GroupExportCsvWorker
-
include Sidekiq::Worker
-
-
def perform(group_id, actor_id)
-
actor = User.find(actor_id)
-
group = Group.find(group_id)
-
csv = GroupExporter.new(group).to_csv
-
filename = "#{group.full_name} CSV export #{DateTime.now.iso8601}".parameterize+".csv"
-
document = Document.new(author: actor, title: filename)
-
document.file.attach(io: StringIO.new(csv), filename: filename)
-
document.save!
-
UserMailer.group_export_ready(actor.id, group.full_name, document.id).deliver
-
DestroyRecordWorker.perform_at(1.week.from_now, 'Document', document.id)
-
end
-
end
-
1
class GroupExportWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(group_ids, group_name, actor_id)
-
1
actor = User.find_by!(id:actor_id)
-
1
groups = Group.where(id: group_ids)
-
1
filename = GroupExportService.export(groups, group_name)
-
1
document = Document.new(author: actor, title: filename)
-
1
document.file.attach(io: File.open(filename), filename: filename)
-
1
document.save!
-
1
UserMailer.group_export_ready(actor.id, group_name, document.id).deliver
-
1
DestroyRecordWorker.perform_at(1.week.from_now, 'Document', document.id)
-
end
-
end
-
class MigrateGuestOnDiscussionReadersAndStances
-
include Sidekiq::Worker
-
-
def perform(group_id)
-
member_ids = Membership.active.where(group_id: group_id).pluck(:user_id)
-
-
DiscussionReader
-
.active.joins(:discussion)
-
.where('discussions.group_id': group_id)
-
.where.not(inviter_id: nil)
-
.where.not(user_id: member_ids)
-
.update_all(guest: true)
-
-
Stance
-
.latest.joins(:poll)
-
.where('polls.group_id': group_id)
-
.where.not(inviter_id: nil)
-
.where.not(participant_id: member_ids)
-
.update_all(guest: true)
-
end
-
end
-
class MigratePollTemplatesWorker
-
include Sidekiq::Worker
-
-
def perform
-
Poll.where(template: true).where("group_id is not null").find_each do |p|
-
pt = PollTemplate.new
-
-
[
-
:group_id,
-
:author_id,
-
:poll_type,
-
:process_name,
-
:process_subtitle,
-
:title,
-
:details,
-
:details_format,
-
:anonymous,
-
:specified_voters_only,
-
:notify_on_closing_soon,
-
:content_locale,
-
:shuffle_options,
-
:hide_results,
-
:chart_type,
-
:min_score,
-
:max_score,
-
:minimum_stance_choices,
-
:maximum_stance_choices,
-
:dots_per_person,
-
:reason_prompt,
-
:stance_reason_required,
-
:limit_reason_length,
-
:agree_target,
-
:created_at,
-
:updated_at,
-
:meeting_duration,
-
:can_respond_maybe,
-
:poll_option_name_format,
-
:tags].each do |field|
-
pt[field] = p.send(field)
-
end
-
-
pt[:default_duration_in_days] = 7
-
-
pt.poll_options = p.poll_options.map do |o|
-
{
-
name: o.name,
-
meaning: o.meaning,
-
prompt: o.prompt,
-
icon: o.icon
-
}
-
end
-
-
pt.save!
-
-
end
-
end
-
end
-
class MigrateTagsWorker
-
include Sidekiq::Worker
-
-
def perform
-
group_ids = []
-
Tagging.where(taggable_type: 'Discussion').pluck(:taggable_id).uniq.each do |discussion_id|
-
tag_ids = Tagging.where(taggable_id: discussion_id, taggable_type: 'Discussion').pluck(:tag_id)
-
names = Tag.where(id: tag_ids).pluck(:name)
-
if d = Discussion.find_by(id: discussion_id)
-
group_ids.push d.group_id
-
d.update_columns(tags: names.uniq)
-
end
-
end
-
-
Tagging.where(taggable_type: 'Poll').pluck(:taggable_id).uniq.each do |poll_id|
-
tag_ids = Tagging.where(taggable_id: poll_id, taggable_type: 'Poll').pluck(:tag_id)
-
names = Tag.where(id: tag_ids).pluck(:name)
-
if p = Poll.find_by(id: poll_id)
-
group_ids.push p.group_id
-
p.update_columns(tags: names.uniq)
-
end
-
end
-
-
group_ids.each {|id| TagService.update_group_tags(id) }
-
Group.where(id: group_ids).where(parent_id: nil).pluck(:id).each {|id| TagService.update_org_tagging_counts(id) }
-
end
-
end
-
1
class MigrateUserWorker
-
1
include Sidekiq::Worker
-
-
1
attr_reader :source, :destination
-
-
1
def perform(source_id, destination_id)
-
2
@source = User.find_by!(id: source_id)
-
2
@destination = User.find_by!(id: destination_id)
-
2
delete_duplicates
-
40
operations.each { |operation| ActiveRecord::Base.connection.execute(operation) }
-
2
migrate_stances
-
2
update_counters
-
2
RedactUserWorker.new.perform(source_id, destination_id, false)
-
2
UserMailer.accounts_merged(destination.id).deliver_later
-
end
-
-
SCHEMA = {
-
1
attachments: :user_id,
-
documents: :author_id,
-
comments: :user_id,
-
reactions: :user_id,
-
discussion_readers: :user_id,
-
discussions: :author_id,
-
events: :user_id,
-
groups: :creator_id,
-
login_tokens: :user_id,
-
membership_requests: [:requestor_id, :responder_id],
-
memberships: [:user_id, :inviter_id],
-
notifications: :user_id,
-
oauth_applications: :owner_id,
-
omniauth_identities: :user_id,
-
outcomes: :author_id,
-
polls: :author_id,
-
versions: :whodunnit
-
}.freeze
-
-
1
def delete_duplicates
-
2
Membership.delete(destination.all_memberships.
-
joins("INNER JOIN memberships source
-
ON source.group_id = memberships.group_id
-
AND source.user_id = #{source.id}").pluck(:"source.id"))
-
-
2
DiscussionReader.delete(destination.discussion_readers.
-
joins("INNER JOIN discussion_readers source
-
ON source.discussion_id = discussion_readers.discussion_id
-
AND source.user_id = #{source.id}").pluck(:"source.id"))
-
end
-
-
1
def operations
-
2
SCHEMA.map do |table, columns|
-
34
Array(columns).map do |column_name|
-
38
"UPDATE #{table} SET #{column_name} = #{destination.id} WHERE #{column_name} = #{source.id}"
-
end
-
end.flatten
-
end
-
-
1
def migrate_stances
-
2
Stance.where(participant: source).update_all(participant_id: destination.id, latest: false)
-
2
Stance.where(participant: destination).update_all(latest: false)
-
-
2
poll_ids = Stance.where(participant: destination).pluck(:poll_id).uniq
-
2
Poll.where(id: poll_ids).each do |poll|
-
1
poll.stances.where(participant: destination).order(:created_at).last.update_attribute(:latest, true)
-
end
-
end
-
-
1
def update_counters
-
2
destination.reload.groups.each do |group|
-
2
group.update_memberships_count
-
2
group.update_admin_memberships_count
-
2
group.update_pending_memberships_count
-
end
-
-
[
-
2
destination.authored_polls,
-
destination.group_polls,
-
destination.participated_polls
-
].flatten.uniq.each(&:update_counts!)
-
-
2
[source, destination].each do |user|
-
4
user.update_memberships_count
-
end
-
2
destination.update_attribute(:sign_in_count, destination.sign_in_count + source.sign_in_count)
-
end
-
end
-
1
class MoveCommentsWorker
-
1
include Sidekiq::Worker
-
1
def perform(event_ids, source_discussion_id, target_discussion_id)
-
5
source_discussion = Discussion.find(source_discussion_id)
-
5
target_discussion = Discussion.find(target_discussion_id)
-
-
# sanitize event_ids (so they cannot be from another discussion), and ensure we have any children
-
5
event_ids = (Event.where(id: event_ids, discussion_id: source_discussion.id).pluck(:id) +
-
Event.where(parent_id: event_ids, discussion_id: source_discussion.id).pluck(:id)).uniq
-
-
5
all_events = Event.where(id: event_ids)
-
5
all_comments = Comment.where(id: Event.where(id: event_ids, eventable_type: 'Comment').pluck(:eventable_id))
-
5
all_polls = Poll.where(id: Event.where(id: event_ids, eventable_type: 'Poll').pluck(:eventable_id))
-
-
# update eventable.discussion_id
-
5
all_comments.update_all(discussion_id: target_discussion.id)
-
5
all_polls.update_all(discussion_id: target_discussion.id)
-
-
# update comment parents
-
5
all_comments.each do |c|
-
6
if c.parent.discussion_id != target_discussion_id
-
4
c.update_columns(parent_id: target_discussion_id, parent_type: 'Discussion')
-
end
-
end
-
-
5
all_events.update(discussion_id: target_discussion_id, sequence_id: nil)
-
-
5
EventService.repair_thread(target_discussion.id)
-
5
EventService.repair_thread(source_discussion.id)
-
-
5
SearchService.reindex_by_discussion_id(target_discussion.id)
-
5
SearchService.reindex_by_discussion_id(source_discussion.id)
-
-
5
ActiveStorage::Attachment.where(record: all_events.map(&:eventable).compact).update_all(group_id: target_discussion.group_id)
-
-
5
MessageChannelService.publish_models(target_discussion.items, group_id: target_discussion.group.id)
-
end
-
end
-
1
class PublishEventWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(event_id)
-
994
Event.sti_find(event_id).trigger!
-
end
-
end
-
1
class RedactUserWorker
-
1
include Sidekiq::Worker
-
-
# we deactivate and redact the user
-
1
def perform(user_id, actor_id, send_email = true)
-
6
user = User.find_by!(id:user_id)
-
6
return if user.email.nil?
-
6
email = user.email
-
6
locale = user.locale
-
6
deactivated_at = user.deactivated_at || DateTime.now
-
6
group_ids = Membership.active.where(user_id: user_id).pluck(:group_id)
-
6
user.uploaded_avatar.purge_later
-
-
6
User.transaction do
-
6
MembershipService.revoke_by_id(group_ids, user_id, actor_id, deactivated_at)
-
-
6
User.where(id: user_id).update_all(
-
is_admin: false,
-
api_key: nil,
-
secret_token: nil,
-
name: nil,
-
email: nil,
-
short_bio: '',
-
username: nil,
-
experiences: {},
-
avatar_kind: "initials",
-
avatar_initials: nil,
-
country: nil,
-
region: nil,
-
city: nil,
-
location: '',
-
email_newsletter: false,
-
unlock_token: nil,
-
current_sign_in_ip: nil,
-
last_sign_in_ip: nil,
-
encrypted_password: nil,
-
reset_password_token: nil,
-
reset_password_sent_at: nil,
-
unsubscribe_token: nil,
-
detected_locale: nil,
-
email_verified: false,
-
legal_accepted_at: nil,
-
# set an email_sha256 so we can identify redacted accounts if someone provides an email
-
email_sha256: Digest::SHA256.hexdigest(email),
-
deactivated_at: deactivated_at,
-
deactivator_id: actor_id
-
)
-
-
6
PaperTrail::Version.where(item_type: 'User', item_id: user_id).delete_all
-
6
Identities::Base.where(user_id: user_id).delete_all
-
6
MembershipRequest.where(requestor_id: user_id, responded_at: nil).delete_all
-
end
-
-
6
NewsletterService.unsubscribe(email)
-
6
UserMailer.redacted(email, locale).deliver_later if send_email
-
6
SearchService.reindex_by_author_id(user.id)
-
end
-
end
-
class RemovePollExpiredFromThreadsWorker
-
include Sidekiq::Worker
-
sidekiq_options queue: :low, retry: false
-
-
def perform(poll_id)
-
p = Poll.find(poll_id)
-
count = Event.where(eventable: p, kind: 'poll_expired').where("discussion_id is not null").
-
update_all(discussion_id: nil, sequence_id: nil, position: 0, position_key: nil)
-
EventService.repair_thread(p.discussion_id) if count > 0
-
puts "count: #{count}, poll_id: #{poll_id}, discussion_id: #{p.discussion_id}"
-
end
-
end
-
1
class RepairThreadWorker
-
1
include Sidekiq::Worker
-
1
sidekiq_options retry: false
-
-
1
def perform(discussion_id)
-
4
EventService.repair_thread(discussion_id)
-
end
-
end
-
class ResetPollStanceDataWorker
-
include Sidekiq::Worker
-
sidekiq_options queue: :low, retry: false
-
-
def perform(poll_id)
-
p = Poll.find(poll_id)
-
# p.reset_latest_stances!
-
p.stances.each(&:update_option_scores!)
-
p.update_counts!
-
end
-
end
-
class RevokeMembershipsOfDeactivatedUsersWorker
-
include Sidekiq::Worker
-
-
def perform
-
User.where.not(deactivated_at: nil).find_each do |user|
-
group_ids = Membership.active.where(user_id: user.id).pluck(:group_id)
-
if group_ids.count > 0
-
MembershipService.revoke_by_id(group_ids, user.id, user.id, user.deactivated_at)
-
end
-
end
-
end
-
end
-
1
class SendDailyCatchUpEmailWorker
-
1
include Sidekiq::Worker
-
1
sidekiq_options retry: false
-
-
1
def perform
-
5
User.distinct.pluck(:time_zone).uniq.each do |zone|
-
5
if Time.find_zone(zone)
-
5
time_in_zone = DateTime.now.in_time_zone(zone)
-
5
if time_in_zone.hour == 6
-
4
days = [7, time_in_zone.wday, (time_in_zone.wday % 2 == 1) ? 8 : nil].compact
-
4
User.distinct.active.verified.where(time_zone: zone).where(email_catch_up_day: days).find_each do |user|
-
2
period = case user.email_catch_up_day
-
when 8 then 'other'
-
1
when 7 then 'daily'
-
else
-
1
'weekly'
-
end
-
2
UserMailer.catch_up(user.id, nil, period).deliver_now
-
end
-
end
-
end
-
end
-
end
-
end
-
class UpdateBlockedDomainsWorker
-
include Sidekiq::Worker
-
-
def perform
-
puts "updating blocked domains"
-
hostsfile = 'https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn/hosts'
-
BlockedDomain.delete_all
-
URI.open(hostsfile, 'r').each do |line|
-
next unless line.starts_with?('0.0.0.0 ')
-
domain = line.split(" ")[1]
-
BlockedDomain.create(name: domain)
-
end
-
puts "updating blocked domains completed"
-
end
-
end
-
-
-
class UpdatePollCountsWorker
-
include Sidekiq::Worker
-
sidekiq_options queue: :low, retry: false
-
-
def perform(poll_id)
-
p = Poll.find(poll_id)
-
p.update_counts!
-
end
-
end
-
1
class UpdateTagWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(group_id, old_name, new_name, color)
-
5
group = Group.find(group_id)
-
5
group_ids = group.id_and_subgroup_ids
-
-
5
if old_name != new_name
-
5
Tag.where(group_id: group_ids, name: new_name).delete_all
-
5
Tag.where(group_id: group_ids, name: old_name).update_all(name: new_name)
-
end
-
-
5
Discussion.where(group_id: group_ids).where.contains(tags: [old_name]).find_each do |d|
-
8
d.tags[d.tags.index(old_name)] = new_name
-
8
d.update_column(:tags, d.tags.uniq)
-
end
-
-
5
Poll.where(group_id: group_ids).where.contains(tags: [old_name]).find_each do |p|
-
8
p.tags[p.tags.index(old_name)] = new_name
-
8
p.update_column(:tags, p.tags.uniq)
-
end
-
-
5
group_ids.each do |group_id|
-
8
TagService.update_group_tags(group_id)
-
end
-
-
5
Tag.where(group_id: group_ids, name: new_name).update_all(color: color)
-
-
5
TagService.update_org_tagging_counts(group.parent_or_self.id)
-
end
-
end
-
# https://www.modern-rails.com/posts/activestorage-analyzers-and-the-openai-transcription-api/
-
class TranscriptionAnalyzer < ActiveStorage::Analyzer::AudioAnalyzer
-
def metadata
-
super.merge(text: @text, language: @language).compact
-
end
-
-
private
-
-
def probe_from(file)
-
super.tap do
-
response = TranscriptionService.transcribe(file)
-
@text = response["text"]
-
@language = response["language"]
-
record = blob.attachments.first.record
-
# record.body += "<p>#{I18n.t('record_modal.audio_transcript', text: @text, locale: record.author.locale)}</p>"
-
record.body += "<p>#{@text}</p>"
-
record.save!
-
MessageChannelService.publish_models(Array(record), group_id: record.group_id, user_id: record.author_id)
-
end
-
end
-
end
-
class EventBus
-
-
def self.configure
-
yield self
-
end
-
-
def self.broadcast(event, *params)
-
listeners[event].each { |listener| listener.call(*params) }
-
end
-
-
def self.listen(*events, &block)
-
events.each { |event| listeners[event].add(block) }
-
end
-
-
def self.deafen(*events, &block)
-
events.each { |event| listeners[event].delete(block) }
-
end
-
-
def self.clear
-
@@listeners = nil
-
end
-
-
def self.listeners
-
@@listeners ||= Hash.new { |hash, key| hash[key] = Set.new }
-
end
-
private_class_method :listeners
-
-
end
-
require 'victor'
-
-
class PieChartSVG
-
SIZE = 512
-
-
def self.from_primitives(scores, colors)
-
slices = []
-
scores.each_with_index do |v, i|
-
slices << {value: (v.to_f / scores.sum) * 100, color: colors[i]}
-
end
-
draw(slices)
-
end
-
-
def self.from_poll(p)
-
draw(pie_slices(p))
-
end
-
-
def self.arc_path(start_angle, end_angle)
-
radius = SIZE/2
-
rad = Math::PI / 180;
-
x1 = radius + (radius * Math.cos(-start_angle * rad));
-
x2 = radius + (radius * Math.cos(-end_angle * rad));
-
y1 = radius + (radius * Math.sin(-start_angle * rad));
-
y2 = radius + (radius * Math.sin(-end_angle * rad));
-
return ["M", radius, radius, "L", x1, y1, "A", radius, radius, 0, (((end_angle - start_angle) > 180) ? 1 : 0), 0, x2, y2, "z"].join(' ');
-
end
-
-
def self.pie_slices(poll)
-
results = PollService.calculate_results(poll, poll.poll_options)
-
results.filter { |r| r[poll.chart_column] }.map do |r|
-
{value: r[poll.chart_column], color: r[:color] }
-
end
-
end
-
-
def self.draw(slices)
-
svg = Victor::SVG.new(width: SIZE, height: SIZE)
-
case slices.length
-
when 0
-
svg.circle cx: SIZE/2, cy: SIZE/2, r: SIZE/2, fill: 'grey'
-
when 1
-
svg.circle cx: SIZE/2, cy: SIZE/2, r: SIZE/2, fill: slices[0][:color]
-
else
-
start = 90
-
slices.each do |slice|
-
angle = (360 * slice[:value]) / 100
-
svg.path d: arc_path(start, start+angle), stroke_width: 0, fill: slice[:color]
-
start += angle
-
end
-
end
-
svg
-
end
-
end
-
# frozen_string_literal: true
-
# thank you to the author! https://github.com/BlazingBBQ/SlackMrkdwn
-
require 'redcarpet'
-
-
class SlackMrkdwn < Redcarpet::Render::Base
-
class << self
-
def from(markdown)
-
renderer = SlackMrkdwn.new
-
Redcarpet::Markdown.new(renderer, strikethrough: true, underline: true, fenced_code_blocks: true).render(markdown)
-
end
-
end
-
-
# Methods where the first argument is the text content
-
[
-
# block-level calls
-
:block_html,
-
-
:autolink,
-
:raw_html,
-
-
:table, :table_row, :table_cell,
-
-
:superscript, :highlight,
-
-
# footnotes
-
:footnotes, :footnote_def, :footnote_ref,
-
-
:hrule,
-
-
# low level rendering
-
:entity, :normal_text,
-
-
:doc_header, :doc_footer,
-
].each do |method|
-
define_method method do |*args|
-
args.first
-
end
-
end
-
-
# Encode Slack restricted characters
-
def preprocess(content)
-
content.gsub('&', '&').gsub('<', '<').gsub('>', '>')
-
end
-
-
def postprocess(content)
-
content.rstrip
-
end
-
-
# ~~strikethrough~~
-
def strikethrough(content)
-
"~#{content}~"
-
end
-
-
# _italic_
-
def underline(content)
-
"_#{content}_"
-
end
-
-
# *italic*
-
def emphasis(content)
-
"_#{content}_"
-
end
-
-
# **bold**
-
def double_emphasis(content)
-
"*#{content}*"
-
end
-
-
# ***bold and italic***
-
def triple_emphasis(content)
-
"*_#{content}_*"
-
end
-
-
# ``` code block ```
-
def block_code(content, _language)
-
"```\n#{content}```\n\n"
-
end
-
-
# > quote
-
def block_quote(content)
-
"> #{content}"
-
end
-
-
# `code`
-
def codespan(content)
-
"`#{content}`"
-
end
-
-
# links
-
def link(link, _title, content)
-
"<#{link}|#{content}>"
-
end
-
-
# list. Called when all list items have been consumed
-
def list(entries, style)
-
entries = format_list(entries, style)
-
remember_last_list_entries(entries)
-
entries
-
end
-
-
# list item
-
def list_item(entry, _style)
-
if @last_entries && entry.end_with?(@last_entries)
-
entry = indent_list_items(entry)
-
@last_entries = nil
-
end
-
entry
-
end
-
-
# 
-
def image(url, _title, _content)
-
link(url, _title, File.basename(url))
-
end
-
-
def paragraph(text)
-
pre_spacing = @last_entries ? "\n" : nil
-
clear_last_list_entries
-
"#{pre_spacing}#{text}\n\n"
-
end
-
-
# # Header
-
def header(text, _header_level)
-
"*#{text}*\n"
-
end
-
-
def linebreak()
-
"\n"
-
end
-
-
private
-
-
def format_list(entries, style)
-
case style
-
when :ordered
-
number_list(entries)
-
when :unordered
-
add_dashes(entries)
-
end
-
end
-
-
def add_dashes(entries)
-
entries.gsub(/^(\S+.*)$/, '- \1')
-
end
-
-
def number_list(entries)
-
count = 0
-
entries.gsub(/^(\S+.*)$/) do
-
match = Regexp.last_match
-
count += 1
-
"#{count}. #{match[0]}"
-
end
-
end
-
-
def remember_last_list_entries(entries)
-
@last_entries = entries
-
end
-
-
def clear_last_list_entries
-
@last_entries = nil
-
end
-
-
def nest_list_entries(entries)
-
entries.gsub(/^(.+)$/, ' \1')
-
end
-
-
def indent_list_items(entry)
-
entry.gsub(@last_entries, nest_list_entries(@last_entries))
-
end
-
end
-
module Loomio
-
module Version
-
def self.current
-
"2.24.0"
-
end
-
end
-
end